File uyuni-tools-git-115.5907f82.obscpio of Package uyuni-tools

07070100000000000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000000C00000000uyuni-tools07070100000001000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001400000000uyuni-tools/.github07070100000002000081A4000003E80000006400000001660688CE0000036B000000000000000000000000000000000000002D00000000uyuni-tools/.github/PULL_REQUEST_TEMPLATE.md<!--
SPDX-FileCopyrightText: 2024 SUSE LLC

SPDX-License-Identifier: Apache-2.0
-->

## What does this PR change?

**add description**

## Test coverage
- No tests: **add explanation**
- No tests: already covered
- Unit tests were added

- [ ] **DONE**

## Links

Issue(s): #

- [ ] **DONE**

## Changelogs

Make sure the changelogs entries you are adding are compliant with https://github.com/uyuni-project/uyuni/wiki/Contributing#changelogs and https://github.com/uyuni-project/uyuni/wiki/Contributing#uyuni-projectuyuni-repository

If you don't need a changelog check, please mark this checkbox:

- [ ] No changelog needed

If you uncheck the checkbox after the PR is created, you will need to re-run `changelog_test` (see below)

# Before you merge

Check [How to branch and merge properly](https://github.com/uyuni-project/uyuni/wiki/How-to-branch-and-merge-properly)!

07070100000003000081A4000003E80000006400000001660688CE00000209000000000000000000000000000000000000002300000000uyuni-tools/.github/dependabot.yml# SPDX-FileCopyrightText: 2024 SUSE LLC
#
# SPDX-License-Identifier: Apache-2.0

# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"
07070100000004000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001E00000000uyuni-tools/.github/workflows07070100000005000081A4000003E80000006400000001660688CE00000617000000000000000000000000000000000000002800000000uyuni-tools/.github/workflows/build.yml# SPDX-FileCopyrightText: 2023 SUSE LLC
#
# SPDX-License-Identifier: Apache-2.0

name: Build

on:
  pull_request:
    types:
      - opened
      - reopened
      - synchronize
  release:
    types:
      - published

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-tags: true
          fetch-depth: 0

      - name: Setup Go ${{ matrix.go-version }}
        uses: actions/setup-go@v5
        with:
          go-version: '1.20'

      - name: Install dependencies
        run: |
          go get ./...

      - name: Compute version
        run: |
          tag=$(git describe --tags --abbrev=0)
          version=$(git describe --tags --abbrev=0 | cut -f 3 -d '-')
          offset=$(git rev-list --count ${tag}..)
          echo "VERSION=$tag-$offset" >> "$GITHUB_ENV"

      - name: Build
        run: |
          mkdir -p ./bin
          go build \
            -tags netgo \
            -ldflags "-X github.com/uyuni-project/uyuni-tools/shared/utils.Version=${{ env.VERSION }}" \
            -o ./bin \
            ./...

      - name: Build nok8s
        run: |
          mkdir -p ./bin
          go build \
            -tags netgo,nok8s \
            -ldflags "-X github.com/uyuni-project/uyuni-tools/shared/utils.Version=${{ env.VERSION }}" \
            -o ./bin \
            ./...

      - name: Test with the Go CLI
        run: go test ./...

      - name: Upload binaries
        uses: actions/upload-artifact@v4
        with:
          name: binaries
          path: ./bin/*
07070100000006000081A4000003E80000006400000001660688CE00000904000000000000000000000000000000000000002D00000000uyuni-tools/.github/workflows/changelogs.yml# SPDX-FileCopyrightText: 2024 SUSE LLC
#
# SPDX-License-Identifier: Apache-2.0
name: Changelogs


on:
  push:
    branches:
      - main
  pull_request:
    types:
      - opened
      - reopened
      - synchronize

jobs:
  changelog_test:
    name: Test changelog entries
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 1
    - id: master
      name: Get modified master changelog files
      uses: Ana06/get-changed-files@v2.3.0
      with:
        filter: '*.changes'
    - name: Fail if the master changelog files are modified
      if: steps.master.outputs.all
      run: |
        echo "Master changelog files cannot be modified directly."
        echo "Please revert your changes on the following master changelog file(s):"
        for file in ${{steps.master.outputs.all}}
        do
          echo "  - $file"
        done
        echo
        echo "See https://github.com/uyuni-project/uyuni/wiki/Contributing for a guide to writing checklogs."
        exit 1
    - id: changelogs
      name: Get modified changelog files
      if: "!contains(github.event.pull_request.body, '[x] No changelog needed')"
      uses: Ana06/get-changed-files@v2.3.0
      with:
        filter: '*.changes.*'
    - name: Fail if no changelog entries are added
      if: steps.changelogs.conclusion == 'success' && steps.changelogs.outputs.added_modified == ''
      run: |
        echo "No changelog entry found. Please add the required changelog entries."
        echo "See https://github.com/uyuni-project/uyuni/wiki/Contributing for a guide to writing checklogs."
        exit 1

  # warns the user if they merged the PR, but the changelog test failed
  warn_user_if_merged:
    name: Warn user if merged
    if: always() && github.event.action == 'closed' && github.event.pull_request.merged == true && needs.changelog_test.result == 'failure'
    needs: changelog_test
    runs-on: ubuntu-latest
    steps:
    - name: Remind the author with a comment
      uses: peter-evans/create-or-update-comment@v4
      with:
        issue-number: ${{ github.event.pull_request.number }}
        body: |
          :warning: No changelog entry has been added. @${{ github.event.pull_request.user.login }}, please add necessary changelog entries with an additional PR.
07070100000007000081A4000003E80000006400000001660688CE00000810000000000000000000000000000000000000003000000000uyuni-tools/.github/workflows/golangci-lint.yml# SPDX-FileCopyrightText: 2024 SUSE LLC
#
# SPDX-License-Identifier: Apache-2.0

name: golangci-lint
on:
  push:
    branches:
      - main
  pull_request:

permissions:
  contents: read
  # Optional: allow read access to pull request. Use with `only-new-issues` option.
  # pull-requests: read

jobs:
  golangci:
    name: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.20'
          cache: false
      - name: golangci-lint
        uses: golangci/golangci-lint-action@v4
        with:
          # Require: The version of golangci-lint to use.
          # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
          # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
          version: v1.54

          # Optional: working directory, useful for monorepos
          # working-directory: somedir

          # Optional: golangci-lint command line arguments.
          #
          # Note: By default, the `.golangci.yml` file should be at the root of the repository.
          # The location of the configuration file can be changed by using `--config=`
          # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 

          # Optional: show only new issues if it's a pull request. The default value is `false`.
          # only-new-issues: true

          # Optional: if set to true, then all caching functionality will be completely disabled,
          #           takes precedence over all other caching options.
          # skip-cache: true

          # Optional: if set to true, then the action won't cache or restore ~/go/pkg.
          # skip-pkg-cache: true

          # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build.
          # skip-build-cache: true

          # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
          # install-mode: "goinstall"
07070100000008000081A4000003E80000006400000001660688CE000001AA000000000000000000000000000000000000002800000000uyuni-tools/.github/workflows/reuse.yml# SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: CC0-1.0

name: REUSE Compliance Check

on:
  push:
    branches:
      - main
  pull_request:
    types:
      - opened
      - reopened
      - synchronize

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: REUSE Compliance Check
      uses: fsfe/reuse-action@v3
07070100000009000081A4000003E80000006400000001660688CE00000092000000000000000000000000000000000000001700000000uyuni-tools/.gitignore# SPDX-FileCopyrightText: 2023 SUSE LLC
#
# SPDX-License-Identifier: Apache-2.0

bin
.vscode/
.idea/
vendor.tar.gz
*.pyc
__pycache__
**/tags
*.mo
0707010000000A000081A4000003E80000006400000001660688CE00000505000000000000000000000000000000000000001A00000000uyuni-tools/.golangci.yml# SPDX-FileCopyrightText: 2024 SUSE LLC
#
# SPDX-License-Identifier: Apache-2.0

run:
  tests: true
  skip-dirs:
    - vendor
    - examples

linters-settings:
  dupl:
    enabled: true
  errcheck:
    enabled: true
  gofmt:
    enabled: true
    simplify: true
  goimports:
    enabled: true
  gocyclo:
    enabled: true
    min-complexity: 10
  godot:
    enabled: true
  golint:
    enabled: true
  ineffassign:
    enabled: true
  maligned:
    enabled: true
  megacheck:
    enabled: true
  misspell:
    enabled: true
  revive:
    rules:
      - name: exported
        arguments:
          - disableStutteringCheck
  staticcheck:
    enabled: false
  stylecheck:
    enabled: true
    checks: ["ST1005", "ST1019"]
  structcheck:
    enabled: true
  typecheck:
    enabled: false
  unused:
    enabled: true
  varcheck:
    enabled: true
  whitespace:
    enabled: true

linters:
  disable-all: true
  enable:
    - unused
    - dupl
    - errcheck
    - errname
      #- errorlint
    - godot
    - gofmt
    - goimports
    - gosimple
      #- gocyclo
    - revive
    - ineffassign
    - govet
      #- lll
      #- megacheck
    - misspell
    - revive
      #- staticcheck
    - stylecheck
      #- unparam
    - unused
    - whitespace

issues:
  include:
    - EXC0012



0707010000000B000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001300000000uyuni-tools/.reuse0707010000000C000081A4000003E80000006400000001660688CE0000030D000000000000000000000000000000000000001800000000uyuni-tools/.reuse/dep5Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: uyuni-tools
Upstream-Contact: Uyuni Project <>
Source: https://github.com/uyuni-project/uyuni-tools

# Sample paragraph, commented out:
#
# Files: src/*
# Copyright: $YEAR $NAME <$CONTACT>
# License: ...

Files: mgradm/shared/ssl/testdata/*
Copyright: 2023 SUSE LLC
License: Apache-2.0

Files: go.mod
Copyright: 2023 SUSE LLC
License: Apache-2.0

Files: go.sum
Copyright: 2023 SUSE LLC
License: Apache-2.0

Files: uyuni-tools.changes* 
Copyright: 2023 SUSE LLC
License: Apache-2.0

Files: uyuni-tools.spec
Copyright: 2023 SUSE LLC
License: Apache-2.0

Files: .tito/tito.props
Copyright: 2023 SUSE LLC
License: Apache-2.0

Files: .tito/packages/*
Copyright: 2023 SUSE LLC
License: Apache-2.0
0707010000000D000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001200000000uyuni-tools/.tito0707010000000E000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001900000000uyuni-tools/.tito/custom0707010000000F000081A4000003E80000006400000001660688CE00000726000000000000000000000000000000000000002300000000uyuni-tools/.tito/custom/custom.py# Copyright (c) 2018 SUSE Linux Products GmbH
# SPDX-FileCopyrightText: 2023 SUSE LLC
#
# SPDX-License-Identifier: GPL-2.0-only

"""
Code for building packages in SUSE that need generated code not tracked in git.
"""
import os

from tito.builder import Builder
from tito.common import  info_out, run_command, debug

class SuseGitExtraGenerationBuilder(Builder):

    def _setup_sources(self):

        Builder._setup_sources(self)
        setup_execution_file_name = "setup.sh"
        setup_file_dir = os.path.join(self.git_root, self.relative_project_dir)
        setup_file_path = os.path.join(setup_file_dir, setup_execution_file_name)
        if os.path.exists(setup_file_path):
            info_out("Executing %s" % setup_file_path)
            output = run_command("[[ -x %s ]] && %s" % (setup_file_path, setup_file_path), True)
            filename = output.split('\n')[-1]
        if filename and os.path.exists(os.path.join(setup_file_dir, filename)):
            info_out("Copying %s to %s" % (os.path.join(setup_file_dir, filename), self.rpmbuild_sourcedir))
            run_command("cp %s %s/" % (os.path.join(setup_file_dir, filename), self.rpmbuild_sourcedir), True)
            self.sources.append(os.path.join(self.rpmbuild_sourcedir, filename))

        source_push = os.path.join(setup_file_dir, "push.sh")
        if os.path.exists(source_push):
            push_path = os.path.join(self.rpmbuild_sourcedir, "push.sh")
            run_command("cp %s %s/" % (source_push, self.rpmbuild_sourcedir), True)
            self.sources.append(push_path)

            run_command(f"sed '/^URL: .*$/aSource10000: push.sh' -i {self.spec_file}")
            cleanup = f"\nsed '/^Source10000: push.sh/d' -i $SRPM_PKG_DIR/{self.spec_file_name}"
            with open(push_path, "a") as fd:
                fd.write(cleanup)
07070100000010000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001B00000000uyuni-tools/.tito/packages07070100000011000081A4000003E80000006400000001660688CE0000000B000000000000000000000000000000000000002700000000uyuni-tools/.tito/packages/uyuni-tools0.1.5-0 ./
07070100000012000081A4000003E80000006400000001660688CE000000CB000000000000000000000000000000000000001D00000000uyuni-tools/.tito/tito.props[buildconfig]
builder = custom.SuseGitExtraGenerationBuilder
tagger = tito.tagger.SUSETagger
changelog_with_email = 0
changelog_do_not_remove_cherrypick = 0
no_default_changelog = 1
lib_dir=.tito/custom
07070100000013000081A4000003E80000006400000001660688CE00000189000000000000000000000000000000000000001300000000uyuni-tools/.vimrc" SPDX-FileCopyrightText: 2023 SUSE LLC
"
" SPDX-License-Identifier: Apache-2.0

" Local vim configuration loaded by https://github.com/LucHermitte/local_vimrc
" For local_vimrc to use this file, ensure .vimrc is in the g:local_vimrc
" list. You can set it like the following in the vim or neovim config:
"
"     let g:local_vimrc = ['.vimrc']

" Set make command
set makeprg=go\ build\ ./...
07070100000014000081A4000003E80000006400000001660688CE00002C5D000000000000000000000000000000000000001400000000uyuni-tools/LICENSE                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
07070100000015000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001500000000uyuni-tools/LICENSES07070100000016000081A4000003E80000006400000001660688CE00002828000000000000000000000000000000000000002400000000uyuni-tools/LICENSES/Apache-2.0.txtApache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.

"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.

"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.

"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).

"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.

"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:

     (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and

     (b) You must cause any modified files to carry prominent notices stating that You changed the files; and

     (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and

     (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.

     You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!)  The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.

Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
07070100000017000081A4000003E80000006400000001660688CE00001B88000000000000000000000000000000000000002100000000uyuni-tools/LICENSES/CC0-1.0.txtCreative Commons Legal Code

CC0 1.0 Universal

    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
    HEREUNDER.

Statement of Purpose

The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").

Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.

For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.

1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:

  i. the right to reproduce, adapt, distribute, perform, display,
     communicate, and translate a Work;
 ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
     likeness depicted in a Work;
 iv. rights protecting against unfair competition in regards to a Work,
     subject to the limitations in paragraph 4(a), below;
  v. rights protecting the extraction, dissemination, use and reuse of data
     in a Work;
 vi. database rights (such as those arising under Directive 96/9/EC of the
     European Parliament and of the Council of 11 March 1996 on the legal
     protection of databases, and under any national implementation
     thereof, including any amended or successor version of such
     directive); and
vii. other similar, equivalent or corresponding rights throughout the
     world based on applicable law or treaty, and any national
     implementations thereof.

2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.

3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.

4. Limitations and Disclaimers.

 a. No trademark or patent rights held by Affirmer are waived, abandoned,
    surrendered, licensed or otherwise affected by this document.
 b. Affirmer offers the Work as-is and makes no representations or
    warranties of any kind concerning the Work, express, implied,
    statutory or otherwise, including without limitation warranties of
    title, merchantability, fitness for a particular purpose, non
    infringement, or the absence of latent or other defects, accuracy, or
    the present or absence of errors, whether or not discoverable, all to
    the greatest extent permissible under applicable law.
 c. Affirmer disclaims responsibility for clearing rights of other persons
    that may apply to the Work or any use thereof, including without
    limitation any person's Copyright and Related Rights in the Work.
    Further, Affirmer disclaims responsibility for obtaining any necessary
    consents, permissions or other rights required for any use of the
    Work.
 d. Affirmer understands and acknowledges that Creative Commons is not a
    party to this document and has no duty or obligation with respect to
    this CC0 or use of the Work.
07070100000018000081A4000003E80000006400000001660688CE000043B9000000000000000000000000000000000000002600000000uyuni-tools/LICENSES/GPL-2.0-only.txtGNU GENERAL PUBLIC LICENSE
Version 2, June 1991

Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.

Preamble

The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.

When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.

To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.

For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.

We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.

Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.

Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.

The precise terms and conditions for copying, distribution and modification follow.

TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.

1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.

You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.

2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:

     a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.

     b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.

     c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.

In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.

3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:

     a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,

     b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,

     c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.

If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.

4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.

5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.

6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.

7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.

It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.

This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.

8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.

9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.

Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.

10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.

NO WARRANTY

11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

END OF TERMS AND CONDITIONS

How to Apply These Terms to Your New Programs

If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.

To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.

     one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author

     This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.

     This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

     You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this when it starts in an interactive mode:

     Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:

     Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.

signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
07070100000019000081A4000003E80000006400000001660688CE00000C58000000000000000000000000000000000000001600000000uyuni-tools/README.md<!--
SPDX-FileCopyrightText: 2023 SUSE LLC

SPDX-License-Identifier: Apache-2.0
-->

[![REUSE status](https://api.reuse.software/badge/git.fsfe.org/reuse/api)](https://api.reuse.software/info/git.fsfe.org/reuse/api)

# Tools to help using Uyuni as containers

**These tools are work in progress**

* `mgradm` used to help user administer Uyuni servers on K8s and Podman
* `mgrctl` used to help user managing Uyuni servers mainly through its API

# Deployment rolling release

**NOTE:** This is rolling releases, meaning it can be broken at any time. Do not use it in production (yet!)

## For Podman deployment
Requirement:
  - openSUSE Leap Micro 15.5
  - Podman installed

*Note that other distros with a recent Podman installed could work, but for now the tool is not packaged for them in OBS.
So you would need to build it locally.*

Add uyuni-tool repository:
```
zypper ar https://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable:/ContainerUtils/openSUSE_Leap_Micro_5.5/ uyuni-container-utils
```

Install `mgradm` package: `transactional-update pkg install mgradm`

Run `mgradm` command to install Uyuni server on Podman:
```
mgradm install podman
```

If you build `uyuni-tools` on your machine, add the `--image registry.opensuse.org/systemsmanagement/uyuni/stable/containers/uyuni/server` option to the install command.
This is not needed when using the package from OBS as it defaulting with this image at build time.

**NOTE**: rolling image url is: registry.opensuse.org/systemsmanagement/uyuni/master/containers/uyuni/server


Other sub-commands are also available. Explore the tool with the help command.

A tool named `mgrctl` is also available with useful commands.

## K3s deployment

For Look at a more details documentation at:

https://github.com/uyuni-project/uyuni/tree/master/containers/doc/server-kubernetes

# Development documentation

## Building

`go build -o ./bin ./...` will produce the binaries in the root folder with `0.0.0` as version.

To produce shell completion scripts for a given shell you can run:

- `./bin/mgradm completion <shell> > $COMPLETION_FILE` for mgradm
- `./bin/mgrctl error completion <shell> > $COMPLETION_FILE` for mgrctl

You'll then need to source the resulting script(s).

As an example, to enable bash completion for mgradm:

`./bin/mgradm completion bash > ./bin/completion`

`. ./bin/completion`

The supported shells are: bash, zsh and fish.

Alternatively, if you have `podman` installed you can run the `build.sh` script to build binaries compatible with any x86_64 linux.
The version will be computed from the last git tag and offset from it.

### Building in Open Build Service

In order to adjust the image, tag and chart to the project the package is built in, add the following at the end of the project configuration:

```
Macros:
%_default_tag yourtag
%_default_image theregistry.org/path/to/the/server
%_default_chart oci://theregistry.org/path/to/the/chart
:Macros
```

### Disabling features at build time

To disable features at build time pass the `-tags` parameter with the following values in a comma-separated list:

* `nok8s`: will disable Kubernetes support
0707010000001A000081ED000003E80000006400000001660688CE0000031A000000000000000000000000000000000000001500000000uyuni-tools/build.sh#!/usr/bin/bash

# SPDX-FileCopyrightText: 2024 SUSE LLC
#
# SPDX-License-Identifier: Apache-2.0
set -e
mkdir -p ./bin

tag=$(git describe --tags --abbrev=0)
version=$(git describe --tags --abbrev=0 | cut -f 3 -d '-')
offset=$(git rev-list --count ${tag}..)

VERSION_NAME=github.com/uyuni-project/uyuni-tools/shared/utils.Version

CGO_ENABLED=0 go build -ldflags "-X ${VERSION_NAME}=${tag}-${offset}" -o ./bin ./...

for shell in "bash" "zsh" "fish"; do
    COMPLETION_FILE="./bin/completion.${shell}"

    # generate and source shell completion scripts for mgradm and mgrctl
    ./bin/mgradm completion ${shell} > "${COMPLETION_FILE}"
    ./bin/mgrctl completion ${shell} >> "${COMPLETION_FILE}"
    ./bin/mgrpxy completion ${shell} >> "${COMPLETION_FILE}"
done

golangci-lint run
echo "DONE"
0707010000001B000081ED000003E80000006400000001660688CE000000D3000000000000000000000000000000000000001C00000000uyuni-tools/extract_strings#!/bin/sh

for MODULE in mgrctl shared; do
    find ${MODULE} -type f -not -name '*_test.go' -name '*.go' | xargs xgettext --keyword="L" --language=C++ --from-code=UTF-8 -o locale/${MODULE}/${MODULE}.pot -
done
0707010000001C000081A4000003E80000006400000001660688CE00000500000000000000000000000000000000000000001300000000uyuni-tools/go.modmodule github.com/uyuni-project/uyuni-tools

go 1.20

require (
	github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2
	github.com/spf13/cobra v1.1.3
)

require (
	github.com/briandowns/spinner v1.23.0 // indirect
	github.com/chai2010/gettext-go v1.0.2 // indirect
	github.com/creack/pty v1.1.17 // indirect
	github.com/fatih/color v1.7.0 // indirect
)

require (
	github.com/fsnotify/fsnotify v1.4.7 // indirect
	github.com/hashicorp/hcl v1.0.0 // indirect
	github.com/inconshreveable/mousetrap v1.0.0 // indirect
	github.com/magiconair/properties v1.8.1 // indirect
	github.com/mattn/go-colorable v0.1.13 // indirect
	github.com/mattn/go-isatty v0.0.19 // indirect
	github.com/mitchellh/mapstructure v1.1.2 // indirect
	github.com/pelletier/go-toml v1.2.0 // indirect
	github.com/rs/zerolog v1.30.0
	github.com/spf13/afero v1.1.2 // indirect
	github.com/spf13/cast v1.3.0 // indirect
	github.com/spf13/jwalterweatherman v1.0.0 // indirect
	github.com/spf13/pflag v1.0.5
	github.com/spf13/viper v1.7.0
	github.com/subosito/gotenv v1.2.0 // indirect
	golang.org/x/sys v0.12.0 // indirect
	golang.org/x/term v0.10.0
	golang.org/x/text v0.3.2 // indirect
	gopkg.in/ini.v1 v1.51.0 // indirect
	gopkg.in/natefinch/lumberjack.v2 v2.2.1
	gopkg.in/yaml.v2 v2.4.0 // indirect
)
0707010000001D000081A4000003E80000006400000001660688CE00008368000000000000000000000000000000000000001300000000uyuni-tools/go.sumcloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=
github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
0707010000001E000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001300000000uyuni-tools/locale0707010000001F000081ED000003E80000006400000001660688CE000001B9000000000000000000000000000000000000001C00000000uyuni-tools/locale/build.shPREFIX=$1

domains=mgrctl
for domain in ${domains}; do
    for po_file in `ls ${domain}/*.po`; do
        lang=$(basename ${po_file} | sed 's/\.po$//')
        locale_dir=${PREFIX}${lang}/LC_MESSAGES
        install -vd -m 0755 ${locale_dir}

        msgcat -o ${locale_dir}/${domain}.po ${po_file} shared/${lang}.po
        msgfmt -o ${locale_dir}/${domain}.mo ${locale_dir}/${domain}.po
        rm ${locale_dir}/${domain}.po
    done
done
07070100000020000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001A00000000uyuni-tools/locale/mgrctl07070100000021000081A4000003E80000006400000001660688CE0000036C000000000000000000000000000000000000002000000000uyuni-tools/locale/mgrctl/fr.po# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-29 09:37+0100\n"
"PO-Revision-Date: 2024-03-29 09:41+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.4.2\n"

#: mgrctl/cmd/cmd.go:30
msgid "Uyuni control tool"
msgstr "Outil de contrôle d'Uyuni"

#: mgrctl/cmd/cmd.go:31
msgid ""
"Uyuni control tool used to help user managing Uyuni and SUSE Manager Servers "
"mainly through its API"
msgstr ""
"Outil pour gérer des serveurs Uyuni et SUSE Manager, principalement via leur "
"API"
07070100000022000081A4000003E80000006400000001660688CE00000319000000000000000000000000000000000000002500000000uyuni-tools/locale/mgrctl/mgrctl.pot# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-29 10:01+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#: mgrctl/cmd/cmd.go:30
msgid "Uyuni control tool"
msgstr ""

#: mgrctl/cmd/cmd.go:31
msgid ""
"Uyuni control tool used to help user managing Uyuni and SUSE Manager Servers "
"mainly through its API"
msgstr ""
07070100000023000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001A00000000uyuni-tools/locale/shared07070100000024000081A4000003E80000006400000001660688CE00000376000000000000000000000000000000000000002000000000uyuni-tools/locale/shared/fr.po# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-29 10:01+0100\n"
"PO-Revision-Date: 2024-03-29 10:09+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.4.2\n"

#: shared/utils/cmd.go:55
msgid ""
"tool to use to reach the container. Possible values: 'podman', 'podman-"
"remote', 'kubectl'. Default guesses which to use."
msgstr ""
"outil à utiliser pour accéder au conteneur. Valeurs possibles: "
"'podman', 'podman-remote', 'kubectl'. Par défaut la valeur à utiliser "
"sera détectée."
07070100000025000081A4000003E80000006400000001660688CE000002F2000000000000000000000000000000000000002500000000uyuni-tools/locale/shared/shared.pot# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-29 10:01+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#: shared/utils/cmd.go:55
msgid ""
"tool to use to reach the container. Possible values: 'podman', 'podman-"
"remote', 'kubectl'. Default guesses which to use."
msgstr ""
07070100000026000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001300000000uyuni-tools/mgradm07070100000027000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001700000000uyuni-tools/mgradm/cmd07070100000028000081A4000003E80000006400000001660688CE00000C59000000000000000000000000000000000000001E00000000uyuni-tools/mgradm/cmd/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
	"os"
	"path"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/completion"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"

	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/distro"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/hub"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/inspect"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/install"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/restart"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/start"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/status"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/stop"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/support"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/uninstall"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade"
)

// NewCommand returns a new cobra.Command implementing the root command for kinder.
func NewUyuniadmCommand() (*cobra.Command, error) {
	globalFlags := &types.GlobalFlags{}
	name := path.Base(os.Args[0])
	rootCmd := &cobra.Command{
		Use:          name,
		Short:        "Uyuni administration tool",
		Long:         "Uyuni administration tool used to help user administer uyuni servers on kubernetes and podman",
		Version:      utils.Version,
		SilenceUsage: true, // Don't show usage help on errors
	}

	usage, err := utils.GetUsageWithConfigHelpTemplate(rootCmd.UsageTemplate())
	if err != nil {
		return rootCmd, err
	}
	rootCmd.SetUsageTemplate(usage)

	rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
		utils.LogInit(true)
		utils.SetLogLevel(globalFlags.LogLevel)

		// do not log if running the completion cmd as the output is redirected to create a file to source
		if cmd.Name() != "completion" {
			log.Info().Msgf("Welcome to %s", name)
			log.Info().Msgf("Executing command: %s", cmd.Name())
		}
	}

	rootCmd.PersistentFlags().StringVarP(&globalFlags.ConfigPath, "config", "c", "", "configuration file path")
	rootCmd.PersistentFlags().StringVar(&globalFlags.LogLevel, "logLevel", "", "application log level (trace|debug|info|warn|error|fatal|panic)")

	migrateCmd := migrate.NewCommand(globalFlags)
	rootCmd.AddCommand(migrateCmd)

	installCmd := install.NewCommand(globalFlags)
	rootCmd.AddCommand(installCmd)

	rootCmd.AddCommand(uninstall.NewCommand(globalFlags))
	distroCmd, err := distro.NewCommand(globalFlags)
	if err != nil {
		return rootCmd, err
	}
	rootCmd.AddCommand(distroCmd)
	rootCmd.AddCommand(completion.NewCommand(globalFlags))
	rootCmd.AddCommand(support.NewCommand(globalFlags))
	rootCmd.AddCommand(start.NewCommand(globalFlags))
	rootCmd.AddCommand(hub.NewCommand(globalFlags))
	rootCmd.AddCommand(restart.NewCommand(globalFlags))
	rootCmd.AddCommand(stop.NewCommand(globalFlags))
	rootCmd.AddCommand(status.NewCommand(globalFlags))
	rootCmd.AddCommand(inspect.NewCommand(globalFlags))
	rootCmd.AddCommand(upgrade.NewCommand(globalFlags))

	return rootCmd, err
}
07070100000029000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001E00000000uyuni-tools/mgradm/cmd/distro0707010000002A000081A4000003E80000006400000001660688CE00000CDD000000000000000000000000000000000000002400000000uyuni-tools/mgradm/cmd/distro/cp.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package distro

import (
	"errors"
	"fmt"
	"os"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func umountAndRemove(mountpoint string) {
	umountCmd := []string{
		"/usr/bin/umount",
		mountpoint,
	}

	if err := utils.RunCmd("/usr/bin/sudo", umountCmd...); err != nil {
		log.Fatal().Err(err).Msgf("Unable to unmount iso file, leaving %s intact", mountpoint)
	}

	os.Remove(mountpoint)
}

func registerDistro(connection *api.ConnectionDetails, distro *types.Distribution) error {
	client, err := api.Init(connection)
	if err != nil {
		log.Error().Msg("Unable to login and register the distribution. Manual distro registration is required.")
		return err
	}
	data := map[string]interface{}{
		"treeLabel":    distro.TreeLabel,
		"basePath":     distro.BasePath,
		"channelLabel": distro.ChannelLabel,
		"installType":  distro.InstallType,
	}

	_, err = client.Post("kickstart/tree/create", data)
	if err != nil {
		return fmt.Errorf("unable to register the distribution. Manual distro registration is required: %s", err)
	}
	log.Info().Msgf("Distribution %s successfully registered", distro.TreeLabel)
	return nil
}

func distroCp(
	globalFlags *types.GlobalFlags,
	flags *flagpole,
	cmd *cobra.Command,
	args []string,
) error {
	distroName := args[1]
	source := args[0]

	var channelLabel string
	if len(args) == 3 {
		channelLabel = args[2]
	} else {
		channelLabel = ""
	}
	cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter)
	log.Info().Msgf("Copying distribution %s\n", distroName)
	if !utils.FileExists(source) {
		return fmt.Errorf("source %s does not exists", source)
	}

	dstpath := "/srv/www/distributions/" + distroName
	if cnx.TestExistenceInPod(dstpath) {
		return fmt.Errorf("distribution already exists: %s", dstpath)
	}

	srcdir := source
	if strings.HasSuffix(source, ".iso") {
		log.Debug().Msg("Source is an iso file")
		tmpdir, err := os.MkdirTemp("", "mgrctl")
		if err != nil {
			return err
		}
		srcdir = tmpdir
		defer umountAndRemove(srcdir)

		mountCmd := []string{
			"/usr/bin/mount",
			"-o", "ro,loop",
			source,
			srcdir,
		}
		if out, err := utils.RunCmdOutput(zerolog.DebugLevel, "/usr/bin/sudo", mountCmd...); err != nil {
			log.Debug().Msgf("Error mounting iso: '%s'", out)
			return errors.New("unable to mount iso file. Mount manually and try again")
		}
	}

	if err := cnx.Copy(srcdir, "server:"+dstpath, "tomcat", "susemanager"); err != nil {
		return fmt.Errorf("cannot copy %s: %s", dstpath, err)
	}

	log.Info().Msg("Distribution has been copied")

	if flags.ConnectionDetails.User != "" {
		distro := types.Distribution{
			BasePath: dstpath,
		}
		if err := detectDistro(srcdir, channelLabel, flags, &distro); err != nil {
			return err
		}

		if err := registerDistro(&flags.ConnectionDetails, &distro); err != nil {
			return err
		}
	}
	return nil
}
0707010000002B000081A4000003E80000006400000001660688CE00000BB9000000000000000000000000000000000000002800000000uyuni-tools/mgradm/cmd/distro/detect.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package distro

import (
	"fmt"
	"path/filepath"

	"github.com/rs/zerolog/log"
	"github.com/spf13/viper"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

var productMap = map[string]map[string]types.Distribution{
	"AlmaLinux": {
		"9": {
			InstallType:  "rhel_9",
			ChannelLabel: "almalinux9",
		},
		"8": {
			InstallType:  "rhel_8",
			ChannelLabel: "almalinux8",
		},
	},

	"SUSE Linux Enterprise": {
		"15 SP1": {
			InstallType:  "sles15generic",
			ChannelLabel: "sle-product-sles15-sp1-pool",
		},
		"15 SP2": {
			InstallType:  "sles15generic",
			ChannelLabel: "sle-product-sles15-sp2-pool",
		},
		"15 SP3": {
			InstallType:  "sles15generic",
			ChannelLabel: "sle-product-sles15-sp3-pool",
		},
		"15 SP4": {
			InstallType:  "sles15generic",
			ChannelLabel: "sle-product-sles15-sp4-pool",
		},
		"15 SP5": {
			InstallType:  "sles15generic",
			ChannelLabel: "sle-product-sles15-sp5-pool",
		},
		"12 SP5": {
			InstallType:  "sles12generic",
			ChannelLabel: "sles12-sp5-pool",
		},
	},

	"Red Hat Enterprise Linux": {
		"7": {
			InstallType:  "rhel_7",
			ChannelLabel: "rhel7-pool",
		},
		"8": {
			InstallType:  "rhel_8",
			ChannelLabel: "rhel8-pool",
		},
		"9": {
			InstallType:  "rhel_9",
			ChannelLabel: "rhel9-pool",
		},
	},
}

func getDistroFromDetails(distro string, version string, arch string, channeLabel string, flags *flagpole) (types.Distribution, error) {
	productFromConfig := flags.ProductMap
	var distribution types.Distribution
	var ok bool

	if productFromConfig[distro] != nil {
		distribution, ok = productFromConfig[distro][version]
	} else if productMap[distro] != nil {
		distribution, ok = productMap[distro][version]
	}

	if !ok {
		return types.Distribution{}, fmt.Errorf("unknown distribution, auto-registration is not possible")
	}

	if channeLabel != "" {
		distribution.ChannelLabel = channeLabel
	} else {
		distribution.ChannelLabel = fmt.Sprintf("%s-%s", distribution.ChannelLabel, arch)
	}

	return distribution, nil
}

func detectDistro(path string, channelLabel string, flags *flagpole, distro *types.Distribution) error {
	treeinfopath := filepath.Join(path, ".treeinfo")
	log.Trace().Msgf("Reading .treeinfo %s", treeinfopath)
	treeInfoViper := viper.New()
	treeInfoViper.SetConfigType("ini")
	treeInfoViper.SetConfigName(".treeinfo")
	treeInfoViper.AddConfigPath(path)
	if err := treeInfoViper.ReadInConfig(); err != nil {
		return err
	}

	dname := treeInfoViper.GetString("release.name")
	dversion := treeInfoViper.GetString("release.version")
	darch := treeInfoViper.GetString("general.arch")
	log.Debug().Msgf("Detected distro %s, version %s. arch %s", dname, dversion, darch)
	details, err := getDistroFromDetails(dname, dversion, darch, channelLabel, flags)
	if err != nil {
		return err
	}

	*distro = types.Distribution{
		InstallType:  details.InstallType,
		TreeLabel:    dname,
		ChannelLabel: details.ChannelLabel,
	}
	return nil
}
0707010000002C000081A4000003E80000006400000001660688CE0000067D000000000000000000000000000000000000002800000000uyuni-tools/mgradm/cmd/distro/distro.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package distro

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type flagpole struct {
	Backend           string
	ChannelLabel      string
	ProductMap        map[string]map[string]types.Distribution
	ConnectionDetails api.ConnectionDetails `mapstructure:"api"`
}

// NewCommand command for distribution management.
func NewCommand(globalFlags *types.GlobalFlags) (*cobra.Command, error) {
	var flags flagpole

	distroCmd := &cobra.Command{
		Use:     "distribution",
		Short:   "Distribution management",
		Long:    "Tools and utilities for distribution management",
		Aliases: []string{"distro"},
	}

	cpCmd := &cobra.Command{
		Use:   "copy path-to-source distribution-name [channel-label]",
		Short: "Copy distribution files from iso to the container",
		Long: `Takes a path to iso file or directory with mounted iso and copies it into the container.

Distribution name specifies the destination directory under /srv/www/distributions.

Optional channel label specify which parent channel to associate with the distribution. Only when API details are provided and auto registration is done.`,
		Args:    cobra.ExactArgs(2),
		Aliases: []string{"cp"},
		RunE: func(cmd *cobra.Command, args []string) error {
			return utils.CommandHelper(globalFlags, cmd, args, &flags, distroCp)
		},
	}

	if err := api.AddAPIFlags(distroCmd, true); err != nil {
		return distroCmd, err
	}
	distroCmd.AddCommand(cpCmd)
	return distroCmd, nil
}
0707010000002D000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001B00000000uyuni-tools/mgradm/cmd/hub0707010000002E000081A4000003E80000006400000001660688CE0000027D000000000000000000000000000000000000002200000000uyuni-tools/mgradm/cmd/hub/hub.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package hub

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/hub/register"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// NewCommand command for Hub management.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	hubCmd := &cobra.Command{
		Use:     "hub",
		Short:   "Hub management",
		Long:    "Tools and utilities for Hub management",
		Aliases: []string{"hub"},
	}

	hubCmd.SetUsageTemplate(hubCmd.UsageTemplate())
	hubCmd.AddCommand(register.NewCommand(globalFlags))
	return hubCmd
}
0707010000002F000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002400000000uyuni-tools/mgradm/cmd/hub/register07070100000030000081A4000003E80000006400000001660688CE00000F03000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/hub/register/register.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package register

import (
	"fmt"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type configFlags struct {
	Backend           string
	ConnectionDetails api.ConnectionDetails `mapstructure:"api"`
}

// NewCommand command for registering peripheral server to hub.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	registerCmd := &cobra.Command{
		Use:   "register",
		Short: "register",
		Long:  "Register this peripheral server to Hub API",
		Args:  cobra.MaximumNArgs(0),

		RunE: func(cmd *cobra.Command, args []string) error {
			var flags configFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, register)
		},
	}
	registerCmd.SetUsageTemplate(registerCmd.UsageTemplate())

	if utils.KubernetesBuilt {
		utils.AddBackendFlag(registerCmd)
	}

	if err := api.AddAPIFlags(registerCmd, false); err != nil {
		return nil
	}

	return registerCmd
}

func register(globalFlags *types.GlobalFlags, flags *configFlags, cmd *cobra.Command, args []string) error {
	cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter)
	config, err := getRhnConfig(cnx)
	if err != nil {
		return err
	}
	err = registerToHub(config, &flags.ConnectionDetails)
	return err
}

func getRhnConfig(cnx *shared.Connection) (map[string]string, error) {
	out, err := cnx.Exec("/bin/cat", "/etc/rhn/rhn.conf")
	if err != nil {
		return nil, err
	}
	config := make(map[string]string)

	lines := strings.Split(string(out), "\n")
	for _, line := range lines {
		if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") {
			continue
		}
		log.Trace().Msgf("Config: %s", line)

		parts := strings.SplitN(line, "=", 2)
		if len(parts) != 2 {
			return nil, fmt.Errorf("invalid line format: %s", line)
		}

		key := strings.TrimSpace(parts[0])
		value := strings.TrimSpace(parts[1])
		config[key] = value
	}

	return config, nil
}

func registerToHub(config map[string]string, cnxDetails *api.ConnectionDetails) error {
	for _, key := range []string{"java.hostname", "report_db_name", "report_db_port", "report_db_user", "report_db_password"} {
		if _, ok := config[key]; !ok {
			return fmt.Errorf("mandatory entry missing in config: %s", key)
		}
	}
	log.Info().Msgf("Hub API server: %s", cnxDetails.Server)
	client, err := api.Init(cnxDetails)
	if err != nil {
		return fmt.Errorf("failed to connect to the Hub server: %s", err)
	}
	data := map[string]interface{}{
		"fqdn": config["java.hostname"],
	}

	ret, err := api.Post[int](client, "system/registerPeripheralServer", data)
	if err != nil {
		return fmt.Errorf("failed to register this peripheral server: %s", err)
	}
	if !ret.Success {
		return fmt.Errorf("failed to register this peripheral server: %s", ret.Message)
	}
	id := ret.Result

	data = map[string]interface{}{
		"sid":              id,
		"reportDbName":     config["report_db_name"],
		"reportDbHost":     config["java.hostname"],
		"reportDbPort":     config["report_db_port"],
		"reportDbUser":     config["report_db_user"],
		"reportDbPassword": config["report_db_password"],
	}
	ret, err = api.Post[int](client, "system/updatePeripheralServerInfo", data)
	if err != nil {
		return fmt.Errorf("failed to update peripheral server info: %s", err)
	}

	if !ret.Success {
		return fmt.Errorf("failed to update peripheral server info: %s", ret.Message)
	}
	log.Info().Msgf("Registered peripheral server: %s, ID: %d", config["java.hostname"], id)
	return nil
}
07070100000031000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/inspect07070100000032000081A4000003E80000006400000001660688CE000005D9000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/inspect/inspect.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package inspect

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"

	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type inspectFlags struct {
	Image      string
	Tag        string
	PullPolicy string
}

// NewCommand for extracting information from image and deployment.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	inspectCmd := &cobra.Command{
		Use:   "inspect",
		Short: "inspect",
		Long:  "Extract information from image and deployment",
		Args:  cobra.MaximumNArgs(0),

		RunE: func(cmd *cobra.Command, args []string) error {
			var flags inspectFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, inspect)
		},
	}

	inspectCmd.SetUsageTemplate(inspectCmd.UsageTemplate())
	inspectCmd.Flags().String("image", "", "Image. Leave it empty to analyze the current deployment")
	inspectCmd.Flags().String("tag", "", "Tag Image. Leave it empty to analyze the current deployment")
	utils.AddPullPolicyFlag(inspectCmd)

	if utils.KubernetesBuilt {
		utils.AddBackendFlag(inspectCmd)
	}

	return inspectCmd
}

func inspect(globalFlags *types.GlobalFlags, flags *inspectFlags, cmd *cobra.Command, args []string) error {
	fn, err := shared.ChoosePodmanOrKubernetes(cmd.Flags(), podmanInspect, kuberneteInspect)
	if err != nil {
		return err
	}
	return fn(globalFlags, flags, cmd, args)
}
07070100000033000081A4000003E80000006400000001660688CE00000F5A000000000000000000000000000000000000002D00000000uyuni-tools/mgradm/cmd/inspect/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package inspect

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"

	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared"
	shared_kubernetes "github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func kuberneteInspect(
	globalFlags *types.GlobalFlags,
	flags *inspectFlags,
	cmd *cobra.Command,
	args []string,
) error {
	serverImage, err := utils.ComputeImage(flags.Image, flags.Tag)
	if err != nil && len(serverImage) > 0 {
		return fmt.Errorf("failed to determine image. %s", err)
	}

	if len(serverImage) <= 0 {
		log.Debug().Msg("Use deployed image")

		cnx := shared.NewConnection("kubectl", "", shared_kubernetes.ServerFilter)
		serverImage, err = adm_utils.RunningImage(cnx, "uyuni")
		if err != nil {
			return fmt.Errorf("failed to find current running image: %s", err)
		}
	}

	inspectResult, err := InspectKubernetes(serverImage, flags.PullPolicy)
	if err != nil {
		return fmt.Errorf("inspect command failed %s", err)
	}

	prettyInspectOutput, err := json.MarshalIndent(inspectResult, "", "  ")
	if err != nil {
		return fmt.Errorf("cannot print inspect result %s", err)
	}

	log.Info().Msgf("\n%s", string(prettyInspectOutput))

	return nil
}

// InspectKubernetes check values on a given image and deploy.
func InspectKubernetes(serverImage string, pullPolicy string) (map[string]string, error) {
	for _, binary := range []string{"kubectl", "helm"} {
		if _, err := exec.LookPath(binary); err != nil {
			return map[string]string{}, fmt.Errorf("install %s before running this command. %s", binary, err)
		}
	}

	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	defer os.RemoveAll(scriptDir)
	if err != nil {
		return map[string]string{}, fmt.Errorf("failed to create temporary directory. %s", err)
	}

	if err := adm_utils.GenerateInspectContainerScript(scriptDir); err != nil {
		return map[string]string{}, err
	}

	command := path.Join(adm_utils.InspectOutputFile.Directory, adm_utils.InspectScriptFilename)

	const podName = "inspector"

	//delete pending pod and then check the node, because in presence of more than a pod GetNode return is wrong
	if err := shared_kubernetes.DeletePod(podName, shared_kubernetes.ServerFilter); err != nil {
		return map[string]string{}, fmt.Errorf("cannot delete %s: %s", podName, err)
	}

	//this is needed because folder with script needs to be mounted
	nodeName, err := shared_kubernetes.GetNode("uyuni")
	if err != nil {
		return map[string]string{}, fmt.Errorf("cannot find node for app uyuni %s", err)
	}

	//generate deploy data
	deployData := types.Deployment{
		APIVersion: "v1",
		Spec: &types.Spec{
			RestartPolicy: "Never",
			NodeName:      nodeName,
			Containers: []types.Container{
				{
					Name: podName,
					VolumeMounts: append(utils.PgsqlRequiredVolumeMounts,
						types.VolumeMount{MountPath: "/var/lib/uyuni-tools", Name: "var-lib-uyuni-tools"}),
					Image: serverImage,
				},
			},
			Volumes: append(utils.PgsqlRequiredVolumes,
				types.Volume{Name: "var-lib-uyuni-tools", HostPath: &types.HostPath{Path: scriptDir, Type: "Directory"}}),
		},
	}
	//transform deploy data in JSON
	override, err := shared_kubernetes.GenerateOverrideDeployment(deployData)
	if err != nil {
		return map[string]string{}, err
	}
	err = shared_kubernetes.RunPod(podName, shared_kubernetes.ServerFilter, serverImage, pullPolicy, command, override)
	if err != nil {
		return map[string]string{}, fmt.Errorf("cannot run inspect pod %s", err)
	}

	inspectResult, err := adm_utils.ReadInspectData(scriptDir)
	if err != nil {
		return map[string]string{}, fmt.Errorf("cannot inspect data. %s", err)
	}

	return inspectResult, err
}
07070100000034000081A4000003E80000006400000001660688CE0000015D000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/inspect/nobuild.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build nok8s

package inspect

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kuberneteInspect(
	globalFlags *types.GlobalFlags,
	flags *inspectFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return nil
}
07070100000035000081A4000003E80000006400000001660688CE00000C38000000000000000000000000000000000000002900000000uyuni-tools/mgradm/cmd/inspect/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package inspect

import (
	"encoding/json"
	"fmt"
	"os"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/podman"
	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared"
	shared_podman "github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func podmanInspect(
	globalFlags *types.GlobalFlags,
	flags *inspectFlags,
	cmd *cobra.Command,
	args []string,
) error {
	serverImage, err := utils.ComputeImage(flags.Image, flags.Tag)
	if err != nil && len(serverImage) > 0 {
		return fmt.Errorf("failed to determine image. %s", err)
	}

	if len(serverImage) <= 0 {
		log.Debug().Msg("Use deployed image")

		cnx := shared.NewConnection("podman", shared_podman.ServerContainerName, "")
		serverImage, err = adm_utils.RunningImage(cnx, shared_podman.ServerContainerName)
		if err != nil {
			return fmt.Errorf("failed to find current running image")
		}
	}
	inspectResult, err := InspectPodman(serverImage, flags.PullPolicy)
	if err != nil {
		return fmt.Errorf("inspect command failed %s", err)
	}
	prettyInspectOutput, err := json.MarshalIndent(inspectResult, "", "  ")
	if err != nil {
		return fmt.Errorf("cannot print inspect result %s", err)
	}

	log.Info().Msgf("\n%s", string(prettyInspectOutput))

	return nil
}

// InspectPodman check values on a given image and deploy.
func InspectPodman(serverImage string, pullPolicy string) (map[string]string, error) {
	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	defer os.RemoveAll(scriptDir)
	if err != nil {
		return map[string]string{}, fmt.Errorf("failed to create temporary directory %s", err)
	}

	inspectedHostValues, err := adm_utils.InspectHost()
	if err != nil {
		return map[string]string{}, fmt.Errorf("cannot inspect host values: %s", err)
	}

	pullArgs := []string{}
	_, scc_user_exist := inspectedHostValues["host_scc_username"]
	_, scc_user_password := inspectedHostValues["host_scc_password"]
	if scc_user_exist && scc_user_password {
		pullArgs = append(pullArgs, "--creds", inspectedHostValues["host_scc_username"]+":"+inspectedHostValues["host_scc_password"])
	}

	preparedImage, err := shared_podman.PrepareImage(serverImage, pullPolicy, pullArgs...)
	if err != nil {
		return map[string]string{}, err
	}

	if err := adm_utils.GenerateInspectContainerScript(scriptDir); err != nil {
		return map[string]string{}, err
	}

	podmanArgs := []string{
		"-v", scriptDir + ":" + adm_utils.InspectOutputFile.Directory,
		"--security-opt", "label:disable",
	}

	err = podman.RunContainer("uyuni-inspect", preparedImage, podmanArgs,
		[]string{adm_utils.InspectOutputFile.Directory + "/" + adm_utils.InspectScriptFilename})
	if err != nil {
		return map[string]string{}, err
	}

	inspectResult, err := adm_utils.ReadInspectData(scriptDir)
	if err != nil {
		return map[string]string{}, fmt.Errorf("cannot inspect data. %s", err)
	}

	return inspectResult, err
}
07070100000036000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/install07070100000037000081A4000003E80000006400000001660688CE00000304000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/install/install.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package install

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/kubernetes"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// NewCommand for installation.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	installCmd := &cobra.Command{
		Use:   "install",
		Short: "install a new server from scratch",
		Long:  "Install a new server from scratch",
	}

	installCmd.AddCommand(podman.NewCommand(globalFlags))

	if kubernetesCmd := kubernetes.NewCommand(globalFlags); kubernetesCmd != nil {
		installCmd.AddCommand(kubernetesCmd)
	}

	return installCmd
}
07070100000038000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/install/kubernetes07070100000039000081A4000003E80000006400000001660688CE000005CD000000000000000000000000000000000000003800000000uyuni-tools/mgradm/cmd/install/kubernetes/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package kubernetes

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/shared"
	cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type kubernetesInstallFlags struct {
	shared.InstallFlags `mapstructure:",squash"`
	Helm                cmd_utils.HelmFlags
}

// NewCommand for kubernetes installation.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	kubernetesCmd := &cobra.Command{
		Use:   "kubernetes [fqdn]",
		Short: "install a new server on a kubernetes cluster from scratch",
		Long: `Install a new server on a kubernetes cluster from scratch

The install command assumes the following:
  * kubectl is installed locally
  * a working kubeconfig should be set to connect to the cluster to deploy to

The helm values file will be overridden with the values from the mgradm parameters or configuration.

NOTE: for now installing on a remote cluster is not supported!
`,
		Args: cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags kubernetesInstallFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, installForKubernetes)
		},
	}

	shared.AddInstallFlags(kubernetesCmd)
	cmd_utils.AddHelmInstallFlag(kubernetesCmd)

	return kubernetesCmd
}
0707010000003A000081A4000003E80000006400000001660688CE00000124000000000000000000000000000000000000003500000000uyuni-tools/mgradm/cmd/install/kubernetes/nobuild.go// SPDX-FileCopyrightText: 2023 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build nok8s

package kubernetes

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	return nil
}
0707010000003B000081A4000003E80000006400000001660688CE00000A6A000000000000000000000000000000000000003300000000uyuni-tools/mgradm/cmd/install/kubernetes/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package kubernetes

import (
	"fmt"
	"os/exec"

	"github.com/rs/zerolog"
	"github.com/spf13/cobra"
	install_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/shared"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl"
	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared"
	shared_kubernetes "github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func installForKubernetes(globalFlags *types.GlobalFlags,
	flags *kubernetesInstallFlags,
	cmd *cobra.Command,
	args []string,
) error {
	for _, binary := range []string{"kubectl", "helm"} {
		if _, err := exec.LookPath(binary); err != nil {
			return fmt.Errorf("install %s before running this command: %s", binary, err)
		}
	}

	flags.CheckParameters(cmd, "kubectl")
	cnx := shared.NewConnection("kubectl", "", shared_kubernetes.ServerFilter)

	fqdn := args[0]

	helmArgs := []string{"--set", "timezone=" + flags.TZ}
	if flags.MirrorPath != "" {
		// TODO Handle claims for multi-node clusters
		helmArgs = append(helmArgs, "--set", "mirror.hostPath="+flags.MirrorPath)
	}
	if flags.Debug.Java {
		helmArgs = append(helmArgs, "--set", "exposeJavaDebug=true")
	}

	// Check the kubernetes cluster setup
	clusterInfos, err := shared_kubernetes.CheckCluster()
	if err != nil {
		return err
	}

	// Deploy the SSL CA or server certificate
	ca := ssl.SslPair{}
	sslArgs, err := kubernetes.DeployCertificate(&flags.Helm, &flags.Ssl, "", &ca, clusterInfos.GetKubeconfig(), fqdn,
		flags.Image.PullPolicy)
	if err != nil {
		return fmt.Errorf("cannot deploy certificate: %s", err)
	}
	helmArgs = append(helmArgs, sslArgs...)

	// Deploy Uyuni and wait for it to be up
	if err := kubernetes.Deploy(cnx, &flags.Image, &flags.Helm, &flags.Ssl, clusterInfos, fqdn, flags.Debug.Java, helmArgs...); err != nil {
		return fmt.Errorf("cannot deploy uyuni: %s", err)
	}

	// Create setup script + env variables and copy it to the container
	envs := map[string]string{
		"NO_SSL": "Y",
	}

	if err := install_shared.RunSetup(cnx, &flags.InstallFlags, args[0], envs); err != nil {
		return err
	}

	// The CA needs to be added to the database for Kickstart use.
	err = adm_utils.ExecCommand(zerolog.DebugLevel, cnx,
		"/usr/bin/rhn-ssl-dbstore", "--ca-cert=/etc/pki/trust/anchors/LOCAL-RHN-ORG-TRUSTED-SSL-CERT")
	if err != nil {
		return fmt.Errorf("error storing the SSL CA certificate in database: %s", err)
	}
	return nil
}
0707010000003C000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/install/podman0707010000003D000081A4000003E80000006400000001660688CE000004A5000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/install/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/shared"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type podmanInstallFlags struct {
	shared.InstallFlags `mapstructure:",squash"`
	Podman              podman.PodmanFlags
}

// NewCommand for podman installation.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	podmanCmd := &cobra.Command{
		Use:   "podman [fqdn]",
		Short: "install a new server on podman from scratch",
		Long: `Install a new server on podman from scratch

The install podman command assumes podman is installed locally

NOTE: for now installing on a remote podman is not supported!
`,
		Args: cobra.MaximumNArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags podmanInstallFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, installForPodman)
		},
	}

	shared.AddInstallFlags(podmanCmd)
	podman.AddPodmanInstallFlag(podmanCmd)

	return podmanCmd
}
0707010000003E000081A4000003E80000006400000001660688CE00001000000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/install/podman/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"fmt"
	"os/exec"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	install_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/shared"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/podman"
	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared"
	shared_podman "github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func waitForSystemStart(cnx *shared.Connection, image string, flags *podmanInstallFlags) error {
	podmanArgs := flags.Podman.Args
	if flags.MirrorPath != "" {
		podmanArgs = append(podmanArgs, "-v", flags.MirrorPath+":/mirror")
	}

	if err := podman.GenerateSystemdService(flags.TZ, image, flags.Debug.Java, podmanArgs); err != nil {
		return fmt.Errorf("cannot generate systemd service: %s", err)
	}

	log.Info().Msg("Waiting for the server to start...")
	if err := shared_podman.EnableService(shared_podman.ServerService); err != nil {
		return fmt.Errorf("cannot enable service: %s", err)
	}

	return cnx.WaitForServer()
}

func installForPodman(
	globalFlags *types.GlobalFlags,
	flags *podmanInstallFlags,
	cmd *cobra.Command,
	args []string,
) error {
	flags.CheckParameters(cmd, "podman")
	if _, err := exec.LookPath("podman"); err != nil {
		return fmt.Errorf("install podman before running this command: %s", err)
	}

	inspectedHostValues, err := adm_utils.InspectHost()
	if err != nil {
		return fmt.Errorf("cannot inspect host values: %s", err)
	}

	fqdn, err := getFqdn(args)
	if err != nil {
		return err
	}
	log.Info().Msgf("setting up server with the FQDN '%s'", fqdn)

	image, err := utils.ComputeImage(flags.Image.Name, flags.Image.Tag)
	if err != nil {
		return fmt.Errorf("failed to compute image URL, %s", err)
	}
	pullArgs := []string{}
	_, scc_user_exist := inspectedHostValues["host_scc_username"]
	_, scc_user_password := inspectedHostValues["host_scc_password"]
	if scc_user_exist && scc_user_password {
		pullArgs = append(pullArgs, "--creds", inspectedHostValues["host_scc_username"]+":"+inspectedHostValues["host_scc_password"])
	}

	preparedImage, err := shared_podman.PrepareImage(image, flags.Image.PullPolicy, pullArgs...)
	if err != nil {
		return err
	}

	if err := shared_podman.LinkVolumes(&flags.Podman.Mounts); err != nil {
		return err
	}

	cnx := shared.NewConnection("podman", shared_podman.ServerContainerName, "")
	if err := waitForSystemStart(cnx, preparedImage, flags); err != nil {
		return fmt.Errorf("cannot wait for system start: %s", err)
	}

	caPassword := flags.Ssl.Password
	if flags.Ssl.UseExisting() {
		// We need to have a password for the generated CA, even though it will be thrown away after install
		caPassword = "dummy"
	}

	env := map[string]string{
		"CERT_O":       flags.Ssl.Org,
		"CERT_OU":      flags.Ssl.OU,
		"CERT_CITY":    flags.Ssl.City,
		"CERT_STATE":   flags.Ssl.State,
		"CERT_COUNTRY": flags.Ssl.Country,
		"CERT_EMAIL":   flags.Ssl.Email,
		"CERT_CNAMES":  strings.Join(append([]string{fqdn}, flags.Ssl.Cnames...), ","),
		"CERT_PASS":    caPassword,
	}

	log.Info().Msg("run setup command in the container")

	if err := install_shared.RunSetup(cnx, &flags.InstallFlags, fqdn, env); err != nil {
		return err
	}

	if flags.Ssl.UseExisting() {
		if err := podman.UpdateSslCertificate(cnx, &flags.Ssl.Ca, &flags.Ssl.Server); err != nil {
			return fmt.Errorf("cannot update ssl certificate: %s", err)
		}
	}

	if err := shared_podman.EnablePodmanSocket(); err != nil {
		return fmt.Errorf("cannot enable podman socket: %s", err)
	}
	return nil
}

func getFqdn(args []string) (string, error) {
	if len(args) == 1 {
		return args[0], nil
	} else {
		fqdn_b, err := utils.RunCmdOutput(zerolog.DebugLevel, "hostname", "-f")
		if err != nil {
			return "", fmt.Errorf("failed to compute server FQDN: %s", err)
		}
		return strings.TrimSpace(string(fqdn_b)), nil
	}
}
0707010000003F000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/install/shared07070100000040000081A4000003E80000006400000001660688CE0000198F000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/install/shared/flags.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package shared

import (
	"fmt"
	"net/mail"
	"regexp"
	"strings"

	"github.com/spf13/cobra"
	cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	apiTypes "github.com/uyuni-project/uyuni-tools/shared/api/types"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// DbFlags can store all values required to connect to a database.
type DbFlags struct {
	Host     string
	Name     string
	Port     int
	User     string
	Password string
	Protocol string
	Provider string
	Admin    struct {
		User     string
		Password string
	}
}

// SccFlags can store SCC Credentials.
type SccFlags struct {
	User     string
	Password string
}

// DebugFlags contains information about enabled/disabled debug.
type DebugFlags struct {
	Java bool
}

// InstallFlags stores all the flags used by install command.
type InstallFlags struct {
	TZ           string
	Email        string
	EmailFrom    string
	IssParent    string
	MirrorPath   string
	Tftp         bool
	Db           DbFlags
	ReportDb     DbFlags
	Ssl          cmd_utils.SslCertFlags
	Scc          SccFlags
	Debug        DebugFlags
	Image        types.ImageFlags `mapstructure:",squash"`
	Admin        apiTypes.User
	Organization string
}

// idChecker verifies that the value is a valid identifier.
func idChecker(value string) bool {
	r := regexp.MustCompile(`^([[:alnum:]]|[._-])+$`)
	if r.MatchString(value) {
		return true
	}
	fmt.Println("Can only contain letters, digits . _ and -")
	return false
}

// emailChecker verifies that the value is a valid email address.
func emailChecker(value string) bool {
	address, err := mail.ParseAddress(value)
	if err != nil || address.Name != "" || strings.ContainsAny(value, "<>") {
		fmt.Println("Not a valid email address")
		return false
	}
	return true
}

// CheckParameters checks parameters for install command.
func (flags *InstallFlags) CheckParameters(cmd *cobra.Command, command string) {
	if flags.Db.Password == "" {
		flags.Db.Password = utils.GetRandomBase64(30)
	}

	if flags.ReportDb.Password == "" {
		flags.ReportDb.Password = utils.GetRandomBase64(30)
	}

	// Make sure we have all the required 3rd party flags or none
	flags.Ssl.CheckParameters()

	// Since we use cert-manager for self-signed certificates on kubernetes we don't need password for it
	if !flags.Ssl.UseExisting() && command == "podman" {
		utils.AskPasswordIfMissing(&flags.Ssl.Password, cmd.Flag("ssl-password").Usage, 0, 0)
	}

	// Use the host timezone if the user didn't define one
	if flags.TZ == "" {
		flags.TZ = utils.GetLocalTimezone()
	}

	utils.AskIfMissing(&flags.Email, cmd.Flag("email").Usage, 0, 0, emailChecker)
	utils.AskIfMissing(&flags.EmailFrom, cmd.Flag("emailfrom").Usage, 0, 0, emailChecker)

	utils.AskIfMissing(&flags.Admin.Login, cmd.Flag("admin-login").Usage, 1, 64, idChecker)
	utils.AskPasswordIfMissing(&flags.Admin.Password, cmd.Flag("admin-password").Usage, 5, 48)
	utils.AskIfMissing(&flags.Admin.Email, cmd.Flag("admin-email").Usage, 1, 128, emailChecker)
	utils.AskIfMissing(&flags.Organization, cmd.Flag("organization").Usage, 3, 128, nil)
}

// AddInstallFlags add flags to installa command.
func AddInstallFlags(cmd *cobra.Command) {
	cmd.Flags().String("tz", "", "Time zone to set on the server. Defaults to the host timezone")
	cmd.Flags().String("email", "admin@example.com", "Administrator e-mail")
	cmd.Flags().String("emailfrom", "admin@example.com", "E-Mail sending the notifications")
	cmd.Flags().String("mirrorPath", "", "Path to mirrored packages mounted on the host")
	cmd.Flags().String("issParent", "", "Inter Server Sync v1 parent fully qualified domain name")
	cmd.Flags().String("db-user", "spacewalk", "Database user")
	cmd.Flags().String("db-password", "", "Database password. Randomly generated by default")
	cmd.Flags().String("db-name", "susemanager", "Database name")
	cmd.Flags().String("db-host", "localhost", "Database host")
	cmd.Flags().Int("db-port", 5432, "Database port")
	cmd.Flags().String("db-protocol", "tcp", "Database protocol")
	cmd.Flags().String("db-admin-user", "", "External database admin user name")
	cmd.Flags().String("db-admin-password", "", "External database admin password")
	cmd.Flags().String("db-provider", "", "External database provider. Possible values 'aws'")

	cmd.Flags().Bool("tftp", true, "Enable TFTP")
	cmd.Flags().String("reportdb-name", "reportdb", "Report database name")
	cmd.Flags().String("reportdb-host", "localhost", "Report database host")
	cmd.Flags().Int("reportdb-port", 5432, "Report database port")
	cmd.Flags().String("reportdb-user", "pythia_susemanager", "Report Database username")
	cmd.Flags().String("reportdb-password", "", "Report database password. Randomly generated by default")

	// For generated CA and certificate
	cmd.Flags().StringSlice("ssl-cname", []string{}, "SSL certificate cnames separated by commas")
	cmd.Flags().String("ssl-country", "DE", "SSL certificate country")
	cmd.Flags().String("ssl-state", "Bayern", "SSL certificate state")
	cmd.Flags().String("ssl-city", "Nuernberg", "SSL certificate city")
	cmd.Flags().String("ssl-org", "SUSE", "SSL certificate organization")
	cmd.Flags().String("ssl-ou", "SUSE", "SSL certificate organization unit")
	cmd.Flags().String("ssl-password", "", "Password for the CA certificate to generate")
	cmd.Flags().String("ssl-email", "ca-admin@example.com", "SSL certificate E-Mail")

	// For SSL 3rd party certificates
	cmd.Flags().StringSlice("ssl-ca-intermediate", []string{}, "Intermediate CA certificate path")
	cmd.Flags().String("ssl-ca-root", "", "Root CA certificate path")
	cmd.Flags().String("ssl-server-cert", "", "Server certificate path")
	cmd.Flags().String("ssl-server-key", "", "Server key path")

	cmd.Flags().String("scc-user", "", "SUSE Customer Center username")
	cmd.Flags().String("scc-password", "", "SUSE Customer Center password")

	cmd.Flags().Bool("debug-java", false, "Enable tomcat and taskomatic remote debugging")
	cmd_utils.AddImageFlag(cmd)

	cmd.Flags().String("admin-login", "admin", "Administrator user name")
	cmd.Flags().String("admin-password", "", "Administrator password")
	cmd.Flags().String("admin-firstName", "Administrator", "First name of the administrator")
	cmd.Flags().String("admin-lastName", "McAdmin", "Last name of the administrator")
	cmd.Flags().String("admin-email", "", "Administrator's email")
	cmd.Flags().String("organization", "Organization", "First organization name")
}
07070100000041000081A4000003E80000006400000001660688CE000003C7000000000000000000000000000000000000003400000000uyuni-tools/mgradm/cmd/install/shared/flags_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package shared

import "testing"

func TestIdChecker(t *testing.T) {
	data := map[string]bool{
		"foo":       true,
		"foo bar":   false,
		"\u798f":    false,
		"foo123._-": true,
		"foo+":      false,
		"foo&":      false,
		"foo'":      false,
		"foo\"":     false,
		"foo`":      false,
		"foo=":      false,
		"foo#":      false,
	}
	for value, expected := range data {
		actual := idChecker(value)
		if actual != expected {
			t.Errorf("%s: expected %v got %v", value, expected, actual)
		}
	}
}

func TestEmailChecker(t *testing.T) {
	data := map[string]bool{
		"root@localhost":           true,
		"joe.hacker@foo.bar.com":   true,
		"<joe.hacker@foo.bar.com>": false,
		"fooo":                     false,
	}
	for value, expected := range data {
		actual := emailChecker(value)
		if actual != expected {
			t.Errorf("%s: expected %v got %v", value, expected, actual)
		}
	}
}
07070100000042000081A4000003E80000006400000001660688CE00000FE8000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/install/shared/shared.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package shared

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/templates"
	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/api/org"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

const setup_name = "setup.sh"

// RunSetup execute the setup.
func RunSetup(cnx *shared.Connection, flags *InstallFlags, fqdn string, env map[string]string) error {
	tmpFolder := generateSetupScript(flags, fqdn, env)
	defer os.RemoveAll(tmpFolder)

	if err := cnx.Copy(filepath.Join(tmpFolder, setup_name), "server:/tmp/setup.sh", "root", "root"); err != nil {
		return fmt.Errorf("cannot copy /tmp/setup.sh: %s", err)
	}

	err := adm_utils.ExecCommand(zerolog.InfoLevel, cnx, "/tmp/setup.sh")
	if err != nil {
		return fmt.Errorf("error running the setup script: %s", err)
	}

	// Call the org.createFirst api if flags are passed
	// This should not happen since the password is queried and enforced
	if flags.Admin.Password != "" {
		apiCnx := api.ConnectionDetails{
			Server:   fqdn,
			Insecure: true, // TODO Get the CA Cert and toggle this to false
		}
		_, err := org.CreateFirst(&apiCnx, flags.Organization, &flags.Admin)
		if err != nil {
			return err
		}
	}

	log.Info().Msg("Server set up")
	return nil
}

// generateSetupScript creates a temporary folder with the setup script to execute in the container.
// The script exports all the needed environment variables and calls uyuni's mgr-setup.
// Podman or kubernetes-specific variables can be passed using extraEnv parameter.
func generateSetupScript(flags *InstallFlags, fqdn string, extraEnv map[string]string) string {
	localHostValues := []string{
		"localhost",
		"127.0.0.1",
		"::1",
		fqdn,
	}

	localDb := utils.Contains(localHostValues, flags.Db.Host)

	dbHost := flags.Db.Host
	reportdbHost := flags.ReportDb.Host

	if localDb {
		dbHost = "localhost"
		if reportdbHost == "" {
			reportdbHost = "localhost"
		}
	}
	env := map[string]string{
		"UYUNI_FQDN":            fqdn,
		"MANAGER_USER":          flags.Db.User,
		"MANAGER_PASS":          flags.Db.Password,
		"MANAGER_ADMIN_EMAIL":   flags.Email,
		"MANAGER_MAIL_FROM":     flags.EmailFrom,
		"MANAGER_ENABLE_TFTP":   boolToString(flags.Tftp),
		"LOCAL_DB":              boolToString(localDb),
		"MANAGER_DB_NAME":       flags.Db.Name,
		"MANAGER_DB_HOST":       dbHost,
		"MANAGER_DB_PORT":       strconv.Itoa(flags.Db.Port),
		"MANAGER_DB_PROTOCOL":   flags.Db.Protocol,
		"REPORT_DB_NAME":        flags.ReportDb.Name,
		"REPORT_DB_HOST":        reportdbHost,
		"REPORT_DB_PORT":        strconv.Itoa(flags.ReportDb.Port),
		"REPORT_DB_USER":        flags.ReportDb.User,
		"REPORT_DB_PASS":        flags.ReportDb.Password,
		"EXTERNALDB_ADMIN_USER": flags.Db.Admin.User,
		"EXTERNALDB_ADMIN_PASS": flags.Db.Admin.Password,
		"EXTERNALDB_PROVIDER":   flags.Db.Provider,
		"ISS_PARENT":            flags.IssParent,
		"ACTIVATE_SLP":          "N", // Deprecated, will be removed soon
		"SCC_USER":              flags.Scc.User,
		"SCC_PASS":              flags.Scc.Password,
	}
	if flags.MirrorPath != "" {
		env["MIRROR_PATH"] = "/mirror"
	}

	// Add the extra environment variables
	for key, value := range extraEnv {
		env[key] = value
	}

	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	if err != nil {
		log.Fatal().Err(err).Msg("Failed to create temporary directory")
	}

	dataTemplate := templates.MgrSetupScriptTemplateData{
		Env:       env,
		DebugJava: flags.Debug.Java,
	}

	scriptPath := filepath.Join(scriptDir, setup_name)
	if err = utils.WriteTemplateToFile(dataTemplate, scriptPath, 0555, true); err != nil {
		log.Fatal().Err(err).Msg("Failed to generate setup script")
	}

	return scriptDir
}

func boolToString(value bool) string {
	if value {
		return "Y"
	}
	return "N"
}
07070100000043000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/migrate07070100000044000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/migrate/kubernetes07070100000045000081A4000003E80000006400000001660688CE000007F9000000000000000000000000000000000000003800000000uyuni-tools/mgradm/cmd/migrate/kubernetes/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package kubernetes

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/shared"
	cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type kubernetesMigrateFlags struct {
	shared.MigrateFlags `mapstructure:",squash"`
	Helm                cmd_utils.HelmFlags
	Ssl                 cmd_utils.SslCertFlags
}

// NewCommand for kubernetes migration.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	migrateCmd := &cobra.Command{
		Use:   "kubernetes [source server FQDN]",
		Short: "migrate a remote server to containers running on a kubernetes cluster",
		Long: `Migrate a remote server to containers running on a kubernetes cluster

This migration command assumes a few things:
  * the SSH configuration for the source server is complete, including user and
    all needed options to connect to the machine,
  * an SSH agent is started and the key to use to connect to the server is added to it,
  * kubectl is installed locally
  * A working kubeconfig should be set to connect to the cluster to deploy to

When migrating a server with a automatically generate SSL Root CA certificate, the private key
password will be required to convert it to RSA in order to be converted into a kubernetes secret.
This is not needed if the source server does not have a generated SSL CA certificate.

NOTE: for now installing on a remote cluster is not supported yet!
`,
		Args: cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags kubernetesMigrateFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, migrateToKubernetes)
		},
	}

	shared.AddMigrateFlags(migrateCmd)
	cmd_utils.AddHelmInstallFlag(migrateCmd)
	migrateCmd.Flags().String("ssl-password", "", "SSL CA generated private key password")

	return migrateCmd
}
07070100000046000081A4000003E80000006400000001660688CE00000124000000000000000000000000000000000000003500000000uyuni-tools/mgradm/cmd/migrate/kubernetes/nobuild.go// SPDX-FileCopyrightText: 2023 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build nok8s

package kubernetes

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	return nil
}
07070100000047000081A4000003E80000006400000001660688CE00001B4A000000000000000000000000000000000000003300000000uyuni-tools/mgradm/cmd/migrate/kubernetes/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package kubernetes

import (
	"encoding/base64"
	"fmt"
	"os"
	"os/exec"
	"path"

	"github.com/rs/zerolog"
	"github.com/spf13/cobra"
	migration_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/shared"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl"
	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared"
	shared_kubernetes "github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func migrateToKubernetes(
	globalFlags *types.GlobalFlags,
	flags *kubernetesMigrateFlags,
	cmd *cobra.Command,
	args []string,
) error {
	for _, binary := range []string{"kubectl", "helm"} {
		if _, err := exec.LookPath(binary); err != nil {
			return fmt.Errorf("install %s before running this command: %s", binary, err)
		}
	}
	cnx := shared.NewConnection("kubectl", "", shared_kubernetes.ServerFilter)

	serverImage, err := utils.ComputeImage(flags.Image.Name, flags.Image.Tag)
	if err != nil {
		return fmt.Errorf("failed to compute image URL")
	}

	fqdn := args[0]

	// Find the SSH Socket and paths for the migration
	sshAuthSocket := migration_shared.GetSshAuthSocket()
	sshConfigPath, sshKnownhostsPath := migration_shared.GetSshPaths()

	// Prepare the migration script and folder
	scriptDir, err := adm_utils.GenerateMigrationScript(fqdn, true)
	if err != nil {
		return fmt.Errorf("failed to generate migration script: %s", err)
	}

	defer os.RemoveAll(scriptDir)

	// We don't need the SSL certs at this point of the migration
	clusterInfos, err := shared_kubernetes.CheckCluster()
	if err != nil {
		return err
	}
	kubeconfig := clusterInfos.GetKubeconfig()
	//TODO: check if we need to handle SELinux policies, as we do in podman

	// Install Uyuni with generated CA cert: an empty struct means no 3rd party cert
	var sslFlags adm_utils.SslCertFlags

	// Deploy for running migration command
	if err := kubernetes.Deploy(cnx, &flags.Image, &flags.Helm, &sslFlags, clusterInfos, fqdn, false,
		"--set", "migration.ssh.agentSocket="+sshAuthSocket,
		"--set", "migration.ssh.configPath="+sshConfigPath,
		"--set", "migration.ssh.knownHostsPath="+sshKnownhostsPath,
		"--set", "migration.dataPath="+scriptDir); err != nil {
		return fmt.Errorf("cannot run deploy: %s", err)
	}

	//this is needed because folder with script needs to be mounted
	//check the node before scaling down
	nodeName, err := shared_kubernetes.GetNode("uyuni")
	if err != nil {
		return fmt.Errorf("cannot find node for app uyuni %s", err)
	}
	// Run the actual migration
	if err := adm_utils.RunMigration(cnx, scriptDir, "migrate.sh"); err != nil {
		return fmt.Errorf("cannot run migration: %s", err)
	}

	tz, oldPgVersion, newPgVersion, err := adm_utils.ReadContainerData(scriptDir)
	if err != nil {
		return fmt.Errorf("cannot read data from container: %s", err)
	}

	// After each command we want to scale to 0
	err = shared_kubernetes.ReplicasTo(shared_kubernetes.ServerFilter, 0)
	if err != nil {
		return fmt.Errorf("cannot set replica to 0: %s", err)
	}

	defer func() {
		// if something is running, we don't need to set replicas to 1
		if _, err = shared_kubernetes.GetNode("uyuni"); err != nil {
			err = shared_kubernetes.ReplicasTo(shared_kubernetes.ServerFilter, 1)
		}
	}()

	setupSslArray, err := setupSsl(&flags.Helm, kubeconfig, scriptDir, flags.Ssl.Password, flags.Image.PullPolicy)
	if err != nil {
		return fmt.Errorf("cannot setup ssl: %s", err)
	}

	helmArgs := []string{
		"--reset-values",
		"--set", "timezone=" + tz,
	}
	helmArgs = append(helmArgs, setupSslArray...)

	// Run uyuni upgrade using the new ssl certificate
	err = kubernetes.UyuniUpgrade(serverImage, flags.Image.PullPolicy, &flags.Helm, kubeconfig, fqdn, clusterInfos.Ingress, helmArgs...)
	if err != nil {
		return fmt.Errorf("cannot upgrade to image %s using new ssl: %s", serverImage, err)
	}

	if err := shared_kubernetes.WaitForDeployment(flags.Helm.Uyuni.Namespace, "uyuni", "uyuni"); err != nil {
		return fmt.Errorf("cannot wait for deployment of %s: %s", serverImage, err)
	}

	err = shared_kubernetes.ReplicasTo(shared_kubernetes.ServerFilter, 0)
	if err != nil {
		return fmt.Errorf("cannot set replica to 0: %s", err)
	}

	if oldPgVersion != newPgVersion {
		if err := kubernetes.RunPgsqlVersionUpgrade(flags.Image, flags.MigrationImage, nodeName, oldPgVersion, newPgVersion); err != nil {
			return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err)
		}
	}

	schemaUpdateRequired := oldPgVersion != newPgVersion
	if err := kubernetes.RunPgsqlFinalizeScript(serverImage, flags.Image.PullPolicy, nodeName, schemaUpdateRequired); err != nil {
		return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err)
	}

	if err := kubernetes.RunPostUpgradeScript(serverImage, flags.Image.PullPolicy, nodeName); err != nil {
		return fmt.Errorf("cannot run post upgrade script: %s", err)
	}

	err = kubernetes.UyuniUpgrade(serverImage, flags.Image.PullPolicy, &flags.Helm, kubeconfig, fqdn, clusterInfos.Ingress, helmArgs...)
	if err != nil {
		return fmt.Errorf("cannot upgrade to image %s: %s", serverImage, err)
	}

	return shared_kubernetes.WaitForDeployment(flags.Helm.Uyuni.Namespace, "uyuni", "uyuni")
}

// updateIssuer replaces the temporary SSL certificate issuer with the source server CA.
// Return additional helm args to use the SSL certificates.
func setupSsl(helm *adm_utils.HelmFlags, kubeconfig string, scriptDir string, password string, pullPolicy string) ([]string, error) {
	caCert := path.Join(scriptDir, "RHN-ORG-TRUSTED-SSL-CERT")
	caKey := path.Join(scriptDir, "RHN-ORG-PRIVATE-SSL-KEY")

	if utils.FileExists(caCert) && utils.FileExists(caKey) {
		key := base64.StdEncoding.EncodeToString(ssl.GetRsaKey(caKey, password))

		// Strip down the certificate text part
		out, err := utils.RunCmdOutput(zerolog.DebugLevel, "openssl", "x509", "-in", caCert)
		if err != nil {
			return []string{}, fmt.Errorf("failed to strip text part of CA certificate %s", err)
		}
		cert := base64.StdEncoding.EncodeToString(out)
		ca := ssl.SslPair{Cert: cert, Key: key}

		// An empty struct means no third party certificate
		sslFlags := adm_utils.SslCertFlags{}
		ret, err := kubernetes.DeployCertificate(helm, &sslFlags, cert, &ca, kubeconfig, "", pullPolicy)
		if err != nil {
			return []string{}, fmt.Errorf("cannot deploy certificate: %s", err)
		}
		return ret, nil
	} else {
		// Handle third party certificates and CA
		sslFlags := adm_utils.SslCertFlags{
			Ca: ssl.CaChain{Root: caCert},
			Server: ssl.SslPair{
				Key:  path.Join(scriptDir, "spacewalk.key"),
				Cert: path.Join(scriptDir, "spacewalk.crt"),
			},
		}
		kubernetes.DeployExistingCertificate(helm, &sslFlags, kubeconfig)
	}
	return []string{}, nil
}
07070100000048000081A4000003E80000006400000001660688CE0000031E000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/migrate/migrate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package migrate

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/kubernetes"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// NewCommand for migration.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	migrateCmd := &cobra.Command{
		Use:   "migrate [source server FQDN]",
		Short: "migrate a remote server to containers",
		Long:  "Migrate a remote server to containers",
	}

	migrateCmd.AddCommand(podman.NewCommand(globalFlags))

	if kubernetesCmd := kubernetes.NewCommand(globalFlags); kubernetesCmd != nil {
		migrateCmd.AddCommand(kubernetesCmd)
	}

	return migrateCmd
}
07070100000049000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/migrate/podman0707010000004A000081A4000003E80000006400000001660688CE000005CD000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/migrate/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/shared"
	podman_utils "github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type podmanMigrateFlags struct {
	shared.MigrateFlags `mapstructure:",squash"`
	Podman              podman_utils.PodmanFlags
}

// NewCommand for podman migration.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	migrateCmd := &cobra.Command{
		Use:   "podman [source server FQDN]",
		Short: "migrate a remote server to containers running on podman",
		Long: `Migrate a remote server to containers running on podman

This migration command assumes a few things:
  * the SSH configuration for the source server is complete, including user and
    all needed options to connect to the machine,
  * an SSH agent is started and the key to use to connect to the server is added to it,
  * podman is installed locally

NOTE: for now installing on a remote podman is not supported yet!
`,
		Args: cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags podmanMigrateFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, migrateToPodman)
		},
	}

	shared.AddMigrateFlags(migrateCmd)
	podman_utils.AddPodmanInstallFlag(migrateCmd)

	return migrateCmd
}
0707010000004B000081A4000003E80000006400000001660688CE00000A10000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/migrate/podman/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"fmt"
	"os/exec"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	migration_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/shared"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/podman"
	podman_utils "github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"

	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func migrateToPodman(globalFlags *types.GlobalFlags, flags *podmanMigrateFlags, cmd *cobra.Command, args []string) error {
	if _, err := exec.LookPath("podman"); err != nil {
		log.Fatal().Err(err).Msg("install podman before running this command")
	}
	sourceFqdn := args[0]
	serverImage, err := utils.ComputeImage(flags.Image.Name, flags.Image.Tag)
	if err != nil {
		return fmt.Errorf("cannot compute image: %s", err)
	}

	// Find the SSH Socket and paths for the migration
	sshAuthSocket := migration_shared.GetSshAuthSocket()
	sshConfigPath, sshKnownhostsPath := migration_shared.GetSshPaths()

	tz, oldPgVersion, newPgVersion, err := podman.RunMigration(serverImage, flags.Image.PullPolicy, sshAuthSocket, sshConfigPath, sshKnownhostsPath, sourceFqdn)
	if err != nil {
		return fmt.Errorf("cannot run migration script: %s", err)
	}

	if oldPgVersion != newPgVersion {
		log.Info().Msgf("Previous postgresql is %s, instead new one is %s. Performing a DB version upgrade...", oldPgVersion, newPgVersion)
		if err := podman.RunPgsqlVersionUpgrade(flags.Image, flags.MigrationImage, oldPgVersion, newPgVersion); err != nil {
			return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err)
		}
	}

	schemaUpdateRequired := oldPgVersion != newPgVersion
	if err := podman.RunPgsqlFinalizeScript(serverImage, schemaUpdateRequired); err != nil {
		return fmt.Errorf("cannot run PostgreSQL finalize script: %s", err)
	}

	if err := podman.RunPostUpgradeScript(serverImage); err != nil {
		return fmt.Errorf("cannot run post upgrade script: %s", err)
	}

	if err := podman.GenerateSystemdService(tz, serverImage, false, viper.GetStringSlice("podman.arg")); err != nil {
		return fmt.Errorf("cannot generate systemd service file: %s", err)
	}

	// Start the service
	if err := podman_utils.EnableService(podman_utils.ServerService); err != nil {
		return err
	}

	log.Info().Msg("Server migrated")

	if err := podman_utils.EnablePodmanSocket(); err != nil {
		return fmt.Errorf("cannot run enable podman socket: %s", err)
	}

	return nil
}
0707010000004C000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/migrate/shared0707010000004D000081A4000003E80000006400000001660688CE0000026B000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/migrate/shared/flags.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package shared

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// MigrateFlags represents flag required by migration command.
type MigrateFlags struct {
	Image          types.ImageFlags `mapstructure:",squash"`
	MigrationImage types.ImageFlags `mapstructure:"migration"`
}

// AddMigrateFlags add migration flags to a command.
func AddMigrateFlags(cmd *cobra.Command) {
	utils.AddImageFlag(cmd)
	utils.AddMigrationImageFlag(cmd)
}
0707010000004E000081A4000003E80000006400000001660688CE00000431000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/migrate/shared/shared.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package shared

import (
	"os"
	"path/filepath"

	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// GetSshAuthSocket returns the SSH_AUTH_SOCK environment variable value.
func GetSshAuthSocket() string {
	path := os.Getenv("SSH_AUTH_SOCK")
	if len(path) == 0 {
		log.Fatal().Msg("SSH_AUTH_SOCK is not defined, start an ssh agent and try again")
	}
	return path
}

// GetSshPaths returns the user SSH config and known_hosts paths.
func GetSshPaths() (string, string) {
	// Find ssh config to mount it in the container
	homedir, err := os.UserHomeDir()
	if err != nil {
		log.Fatal().Msg("Failed to find home directory to look for SSH config")
	}
	sshConfigPath := filepath.Join(homedir, ".ssh", "config")
	sshKnownhostsPath := filepath.Join(homedir, ".ssh", "known_hosts")

	if !utils.FileExists(sshConfigPath) {
		sshConfigPath = ""
	}

	if !utils.FileExists(sshKnownhostsPath) {
		sshKnownhostsPath = ""
	}

	return sshConfigPath, sshKnownhostsPath
}
0707010000004F000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/restart07070100000050000081A4000003E80000006400000001660688CE000001C1000000000000000000000000000000000000002D00000000uyuni-tools/mgradm/cmd/restart/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package restart

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesRestart(
	globalFlags *types.GlobalFlags,
	flags *restartFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return kubernetes.Restart(kubernetes.ServerFilter)
}
07070100000051000081A4000003E80000006400000001660688CE00000191000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/restart/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build nok8s

package restart

import (
	"fmt"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesRestart(
	globalFlags *types.GlobalFlags,
	flags *restartFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return fmt.Errorf("built without kubernetes support")
}
07070100000052000081A4000003E80000006400000001660688CE000001A6000000000000000000000000000000000000002900000000uyuni-tools/mgradm/cmd/restart/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package restart

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func podmanRestart(
	globalFlags *types.GlobalFlags,
	flags *restartFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return podman.RestartService(podman.ServerService)
}
07070100000053000081A4000003E80000006400000001660688CE0000048C000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/restart/restart.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package restart

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type restartFlags struct {
	Backend string
}

// NewCommand to restart server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	restartCmd := &cobra.Command{
		Use:   "restart",
		Short: "restart the server",
		Long:  "Restart the server",
		Args:  cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags restartFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, restart)
		},
	}
	restartCmd.SetUsageTemplate(restartCmd.UsageTemplate())

	if utils.KubernetesBuilt {
		utils.AddBackendFlag(restartCmd)
	}

	return restartCmd
}

func restart(globalFlags *types.GlobalFlags, flags *restartFlags, cmd *cobra.Command, args []string) error {
	fn, err := shared.ChoosePodmanOrKubernetes(cmd.Flags(), podmanRestart, kubernetesRestart)
	if err != nil {
		return err
	}

	return fn(globalFlags, flags, cmd, args)
}
07070100000054000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001D00000000uyuni-tools/mgradm/cmd/start07070100000055000081A4000003E80000006400000001660688CE000001B9000000000000000000000000000000000000002B00000000uyuni-tools/mgradm/cmd/start/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package start

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesStart(
	globalFlags *types.GlobalFlags,
	flags *startFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return kubernetes.Start(kubernetes.ServerFilter)
}
07070100000056000081A4000003E80000006400000001660688CE0000018B000000000000000000000000000000000000002D00000000uyuni-tools/mgradm/cmd/start/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build nok8s

package start

import (
	"fmt"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesStart(
	globalFlags *types.GlobalFlags,
	flags *startFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return fmt.Errorf("built without kubernetes support")
}
07070100000057000081A4000003E80000006400000001660688CE0000019E000000000000000000000000000000000000002700000000uyuni-tools/mgradm/cmd/start/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package start

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func podmanStart(
	globalFlags *types.GlobalFlags,
	flags *startFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return podman.StartService(podman.ServerService)
}
07070100000058000081A4000003E80000006400000001660688CE0000046C000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/start/start.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package start

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type startFlags struct {
	Backend string
}

// NewCommand starts the server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	startCmd := &cobra.Command{
		Use:   "start",
		Short: "start the server",
		Long:  "Start the server",
		Args:  cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags startFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, start)
		},
	}
	startCmd.SetUsageTemplate(startCmd.UsageTemplate())

	if utils.KubernetesBuilt {
		utils.AddBackendFlag(startCmd)
	}

	return startCmd
}

func start(globalFlags *types.GlobalFlags, flags *startFlags, cmd *cobra.Command, args []string) error {
	fn, err := shared.ChoosePodmanOrKubernetes(cmd.Flags(), podmanStart, kubernetesStart)
	if err != nil {
		return err
	}

	return fn(globalFlags, flags, cmd, args)
}
07070100000059000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001E00000000uyuni-tools/mgradm/cmd/status0707010000005A000081A4000003E80000006400000001660688CE0000072B000000000000000000000000000000000000002C00000000uyuni-tools/mgradm/cmd/status/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package status

import (
	"errors"
	"fmt"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesStatus(
	globalFlags *types.GlobalFlags,
	flags *statusFlags,
	cmd *cobra.Command,
	args []string,
) error {
	// Do we have an uyuni helm release?
	clusterInfos, err := kubernetes.CheckCluster()
	if err != nil {
		return fmt.Errorf("failed to discover the cluster type: %s", err)
	}

	kubeconfig := clusterInfos.GetKubeconfig()
	if !kubernetes.HasHelmRelease("uyuni", kubeconfig) {
		return errors.New("no uyuni helm release installed on the cluster")
	}

	namespace, err := kubernetes.FindNamespace("uyuni", kubeconfig)
	if err != nil {
		return fmt.Errorf("failed to find the uyuni deployment namespace: %s", err)
	}

	// Is the pod running? Do we have all the replicas?
	status, err := kubernetes.GetDeploymentStatus(namespace, "uyuni")
	if err != nil {
		return fmt.Errorf("failed to get deployment status: %s", err)
	}
	if status.Replicas != status.ReadyReplicas {
		log.Warn().Msgf("Some replicas are not ready: %d / %d", status.ReadyReplicas, status.Replicas)
	}

	if status.AvailableReplicas == 0 {
		return errors.New("the pod is not running")
	}

	// Are the services running in the container?
	cnx := shared.NewConnection("kubectl", "", kubernetes.ServerFilter)
	if err := adm_utils.ExecCommand(zerolog.InfoLevel, cnx, "spacewalk-service", "status"); err != nil {
		return fmt.Errorf("failed to run spacewalk-service status: %s", err)
	}
	return nil
}
0707010000005B000081A4000003E80000006400000001660688CE0000018E000000000000000000000000000000000000002E00000000uyuni-tools/mgradm/cmd/status/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build nok8s

package status

import (
	"fmt"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesStatus(
	globalFlags *types.GlobalFlags,
	flags *statusFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return fmt.Errorf("built without kubernetes support")
}
0707010000005C000081A4000003E80000006400000001660688CE000004B8000000000000000000000000000000000000002800000000uyuni-tools/mgradm/cmd/status/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package status

import (
	"fmt"

	"github.com/rs/zerolog"
	"github.com/spf13/cobra"
	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func podmanStatus(
	globalFlags *types.GlobalFlags,
	flags *statusFlags,
	cmd *cobra.Command,
	args []string,
) error {
	// Show the status and that's it if the service is not running
	if !podman.IsServiceRunning(podman.ServerService) {
		if err := utils.RunCmdStdMapping(zerolog.DebugLevel, "systemctl", "status", podman.ServerService); err != nil {
			return fmt.Errorf("failed to get status of the server service: %s", err)
		}
		return nil
	}

	// Run spacewalk-service status in the container
	cnx := shared.NewConnection("podman", podman.ServerContainerName, "")
	if err := adm_utils.ExecCommand(zerolog.InfoLevel, cnx, "spacewalk-service", "status"); err != nil {
		return fmt.Errorf("failed to run spacewalk-service status: %s", err)
	}

	return nil
}
0707010000005D000081A4000003E80000006400000001660688CE000004AC000000000000000000000000000000000000002800000000uyuni-tools/mgradm/cmd/status/status.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package status

import (
	"errors"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type statusFlags struct {
}

// NewCommand to get the status of the server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "status",
		Short: "get the server status",
		Long:  "Get the server status",
		Args:  cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags statusFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, status)
		},
	}
	cmd.SetUsageTemplate(cmd.UsageTemplate())

	return cmd
}

func status(globalFlags *types.GlobalFlags, flags *statusFlags, cmd *cobra.Command, args []string) error {
	if podman.HasService(podman.ServerService) {
		return podmanStatus(globalFlags, flags, cmd, args)
	}

	if utils.IsInstalled("kubectl") && utils.IsInstalled("helm") {
		return kubernetesStatus(globalFlags, flags, cmd, args)
	}

	return errors.New("no installed server detected")
}
0707010000005E000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001C00000000uyuni-tools/mgradm/cmd/stop0707010000005F000081A4000003E80000006400000001660688CE000001B5000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/stop/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package stop

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesStop(
	globalFlags *types.GlobalFlags,
	flags *stopFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return kubernetes.Stop(kubernetes.ServerFilter)
}
07070100000060000081A4000003E80000006400000001660688CE00000188000000000000000000000000000000000000002C00000000uyuni-tools/mgradm/cmd/stop/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build nok8s

package stop

import (
	"fmt"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesStop(
	globalFlags *types.GlobalFlags,
	flags *stopFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return fmt.Errorf("built without kubernetes support")
}
07070100000061000081A4000003E80000006400000001660688CE0000019A000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/stop/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package stop

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func podmanStop(
	globalFlags *types.GlobalFlags,
	flags *stopFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return podman.StopService(podman.ServerService)
}
07070100000062000081A4000003E80000006400000001660688CE0000045A000000000000000000000000000000000000002400000000uyuni-tools/mgradm/cmd/stop/stop.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package stop

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type stopFlags struct {
	Backend string
}

// NewCommand to stop server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	stopCmd := &cobra.Command{
		Use:   "stop",
		Short: "stop the server",
		Long:  "Stop the server",
		Args:  cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags stopFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, stop)
		},
	}

	stopCmd.SetUsageTemplate(stopCmd.UsageTemplate())

	if utils.KubernetesBuilt {
		utils.AddBackendFlag(stopCmd)
	}

	return stopCmd
}

func stop(globalFlags *types.GlobalFlags, flags *stopFlags, cmd *cobra.Command, args []string) error {
	fn, err := shared.ChoosePodmanOrKubernetes(cmd.Flags(), podmanStop, kubernetesStop)
	if err != nil {
		return err
	}

	return fn(globalFlags, flags, cmd, args)
}
07070100000063000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/support07070100000064000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/support/config07070100000065000081A4000003E80000006400000001660688CE000003C4000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/support/config/config.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package config

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type configFlags struct {
	Output  string
	Backend string
}

// NewCommand is the command for creates supportconfig.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	configCmd := &cobra.Command{
		Use:   "config",
		Short: "extract configuration and logs",
		Long: `Extract the host or cluster configuration and logs as well as those from 
the containers for support to help debugging.`,
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags configFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, extract)
		},
	}

	configCmd.Flags().StringP("output", "o", "supportconfig.tar.gz", "path where to extract the data")
	utils.AddBackendFlag(configCmd)

	return configCmd
}
07070100000066000081A4000003E80000006400000001660688CE00000C62000000000000000000000000000000000000003300000000uyuni-tools/mgradm/cmd/support/config/extractor.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package config

import (
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path"
	"regexp"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func extract(globalFlags *types.GlobalFlags, flags *configFlags, cmd *cobra.Command, args []string) error {
	cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter)

	// Copy the generated file locally
	tmpDir, err := os.MkdirTemp("", "mgradm-*")
	if err != nil {
		return fmt.Errorf("failed to create temporary directory: %s", err)
	}
	defer os.RemoveAll(tmpDir)

	var files []string
	extensions := []string{"", ".md5"}

	// Run supportconfig in the container if it's running
	log.Info().Msg("Running supportconfig in the container")
	out, err := cnx.Exec("supportconfig")
	if err != nil {
		return errors.New("failed to run supportconfig")
	} else {
		tarballPath := getSupportConfigPath(out)
		if tarballPath == "" {
			return fmt.Errorf("failed to find container supportconfig tarball from command output")
		}

		// TODO Get the error from copy
		for _, ext := range extensions {
			containerTarball := path.Join(tmpDir, "container-supportconfig.txz"+ext)
			if err := cnx.Copy("server:"+tarballPath+ext, containerTarball, "", ""); err != nil {
				return fmt.Errorf("cannot copy tarball: %s", err)
			}
			files = append(files, containerTarball)

			// Remove the generated file in the container
			if _, err := cnx.Exec("rm", tarballPath+ext); err != nil {
				return fmt.Errorf("failed to remove %s%s file in the container: %s", tarballPath, ext, err)
			}
		}
	}

	// Run supportconfig on the host if installed
	if _, err := exec.LookPath("supportconfig"); err == nil {
		out, err := utils.RunCmdOutput(zerolog.DebugLevel, "supportconfig")
		if err != nil {
			return fmt.Errorf("failed to run supportconfig on the host: %s", err)
		}
		tarballPath := getSupportConfigPath(out)

		// Look for the generated supportconfig file
		if tarballPath != "" && utils.FileExists(tarballPath) {
			for _, ext := range extensions {
				files = append(files, tarballPath+ext)
			}
		} else {
			return errors.New("failed to find host supportconfig tarball from command output")
		}
	} else {
		log.Warn().Msg("supportconfig is not available on the host, skipping it")
	}

	// TODO Get cluster infos in case of kubernetes

	// Pack it all into a tarball
	log.Info().Msg("Preparing the tarball")
	tarball, err := utils.NewTarGz(flags.Output)
	if err != nil {
		return err
	}

	for _, file := range files {
		if err := tarball.AddFile(file, path.Base(file)); err != nil {
			return fmt.Errorf("failed to add %s to tarball: %s", path.Base(file), err)
		}
	}
	tarball.Close()

	return nil
}

func getSupportConfigPath(out []byte) string {
	re := regexp.MustCompile(`/var/log/scc_[^.]+\.txz`)
	return re.FindString(string(out))
}
07070100000067000081A4000003E80000006400000001660688CE00000245000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/support/support.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package support

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/support/config"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// NewCommand to export supportconfig.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	supportCmd := &cobra.Command{
		Use:   "support",
		Short: "commands for support operations",
		Long:  "Commands for support operations",
	}
	supportCmd.AddCommand(config.NewCommand(globalFlags))

	return supportCmd
}
07070100000068000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002100000000uyuni-tools/mgradm/cmd/uninstall07070100000069000081A4000003E80000006400000001660688CE00000A82000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/uninstall/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package uninstall

import (
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func uninstallForKubernetes(
	globalFlags *types.GlobalFlags,
	flags *uninstallFlags,
	cmd *cobra.Command,
	args []string,
) error {
	clusterInfos, err := kubernetes.CheckCluster()
	if err != nil {
		return err
	}
	kubeconfig := clusterInfos.GetKubeconfig()

	// TODO Find all the PVs related to the server if we want to delete them

	// Uninstall uyuni
	namespace, err := kubernetes.HelmUninstall(kubeconfig, "uyuni", "", !flags.Force)
	if err != nil {
		return err
	}

	// Remove the remaining configmap and secrets
	if namespace != "" {
		_, err := utils.RunCmdOutput(zerolog.TraceLevel, "kubectl", "-n", namespace, "get", "secret", "uyuni-ca")
		caSecret := "uyuni-ca"
		if err != nil {
			caSecret = ""
		}

		if !flags.Force {
			log.Info().Msgf("Would run kubectl delete -n %s configmap uyuni-ca", namespace)
			log.Info().Msgf("Would run kubectl delete -n %s secret uyuni-cert %s", namespace, caSecret)
		} else {
			log.Info().Msgf("Running kubectl delete -n %s configmap uyuni-ca", namespace)
			if err := utils.RunCmd("kubectl", "delete", "-n", namespace, "configmap", "uyuni-ca"); err != nil {
				log.Info().Err(err).Msgf("Failed deleting config map")
			}

			log.Info().Msgf("Running kubectl delete -n %s secret uyuni-cert %s", namespace, caSecret)

			args := []string{"delete", "-n", namespace, "secret", "uyuni-cert"}
			if caSecret != "" {
				args = append(args, caSecret)
			}
			err := utils.RunCmd("kubectl", args...)
			if err != nil {
				log.Info().Err(err).Msgf("Failed deleting secret")
			}
		}
	}

	// TODO Remove the PVs or wait for their automatic removal if purge is requested
	// Also wait if the PVs are dynamic with Delete reclaim policy but the user didn't ask to purge them
	// Since some storage plugins don't handle Delete policy, we may need to check for error events to avoid infinite loop

	// Uninstall cert-manager if we installed it
	if _, err := kubernetes.HelmUninstall(kubeconfig, "cert-manager", "-linstalledby=mgradm", !flags.Force); err != nil {
		return err
	}

	// Remove the K3s Traefik config
	if clusterInfos.IsK3s() {
		kubernetes.UninstallK3sTraefikConfig(!flags.Force)
	}

	// Remove the rke2 nginx config
	if clusterInfos.IsRke2() {
		kubernetes.UninstallRke2NginxConfig(!flags.Force)
	}
	return nil
}

const kubernetesHelp = kubernetes.UninstallHelp
0707010000006A000081A4000003E80000006400000001660688CE00000182000000000000000000000000000000000000003100000000uyuni-tools/mgradm/cmd/uninstall/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build nok8s

package uninstall

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func uninstallForKubernetes(
	globalFlags *types.GlobalFlags,
	flags *uninstallFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return nil
}

const kubernetesHelp = ""
0707010000006B000081A4000003E80000006400000001660688CE00000461000000000000000000000000000000000000002B00000000uyuni-tools/mgradm/cmd/uninstall/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package uninstall

import (
	"fmt"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func uninstallForPodman(
	globalFlags *types.GlobalFlags,
	flags *uninstallFlags,
	cmd *cobra.Command,
	args []string,
) error {
	// Uninstall the service
	podman.UninstallService("uyuni-server", !flags.Force)

	// Force stop the pod
	podman.DeleteContainer(podman.ServerContainerName, !flags.Force)

	// Remove the volumes
	if flags.PurgeVolumes {
		volumes := []string{"cgroup"}
		for _, volume := range utils.ServerVolumeMounts {
			volumes = append(volumes, volume.Name)
		}
		for _, volume := range volumes {
			if err := podman.DeleteVolume(volume, !flags.Force); err != nil {
				return fmt.Errorf("cannot delete volume %s: %s", volume, err)
			}
		}
		log.Info().Msg("All volumes removed")
	}

	podman.DeleteNetwork(!flags.Force)

	return podman.ReloadDaemon(!flags.Force)
}
0707010000006C000081A4000003E80000006400000001660688CE000005F4000000000000000000000000000000000000002E00000000uyuni-tools/mgradm/cmd/uninstall/uninstall.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package uninstall

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type uninstallFlags struct {
	Backend      string
	Force        bool
	PurgeVolumes bool
}

// NewCommand uninstall a server and optionally the corresponding volumes.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	uninstallCmd := &cobra.Command{
		Use:   "uninstall",
		Short: "uninstall a server",
		Long: `Uninstall a server and optionally the corresponding volumes.
By default it will only print what would be done, use --force to actually remove.` + kubernetesHelp,
		Args: cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags uninstallFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, uninstall)
		},
	}
	uninstallCmd.Flags().BoolP("force", "f", false, "Actually remove the server")
	uninstallCmd.Flags().Bool("purgeVolumes", false, "Also remove the volumes")

	if utils.KubernetesBuilt {
		utils.AddBackendFlag(uninstallCmd)
	}

	return uninstallCmd
}

func uninstall(
	globalFlags *types.GlobalFlags,
	flags *uninstallFlags,
	cmd *cobra.Command,
	args []string,
) error {
	fn, err := shared.ChoosePodmanOrKubernetes(cmd.Flags(), uninstallForPodman, uninstallForKubernetes)
	if err != nil {
		return err
	}

	return fn(globalFlags, flags, cmd, args)
}
0707010000006D000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/upgrade0707010000006E000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/upgrade/kubernetes0707010000006F000081A4000003E80000006400000001660688CE0000044F000000000000000000000000000000000000003800000000uyuni-tools/mgradm/cmd/upgrade/kubernetes/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package kubernetes

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/shared"
	cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type kubernetesUpgradeFlags struct {
	shared.UpgradeFlags `mapstructure:",squash"`
	Helm                cmd_utils.HelmFlags
}

// NewCommand to upgrade a kubernetes server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	upgradeCmd := &cobra.Command{
		Use:   "kubernetes",
		Short: "upgrade a local server on kubernetes",
		Long: `Upgrade a local server on kubernetes
`,
		Args: cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags kubernetesUpgradeFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, upgradeKubernetes)
		},
	}

	shared.AddUpgradeFlags(upgradeCmd)
	cmd_utils.AddHelmInstallFlag(upgradeCmd)

	return upgradeCmd
}
07070100000070000081A4000003E80000006400000001660688CE00000124000000000000000000000000000000000000003500000000uyuni-tools/mgradm/cmd/upgrade/kubernetes/nobuild.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build nok8s

package kubernetes

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	return nil
}
07070100000071000081A4000003E80000006400000001660688CE00001049000000000000000000000000000000000000003300000000uyuni-tools/mgradm/cmd/upgrade/kubernetes/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package kubernetes

import (
	"fmt"
	"os"
	"os/exec"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/inspect"
	upgrade_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/shared"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared"
	shared_kubernetes "github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func upgradeKubernetes(
	globalFlags *types.GlobalFlags,
	flags *kubernetesUpgradeFlags,
	cmd *cobra.Command,
	args []string,
) error {
	for _, binary := range []string{"kubectl", "helm"} {
		if _, err := exec.LookPath(binary); err != nil {
			return fmt.Errorf("install %s before running this command", binary)
		}
	}
	cnx := shared.NewConnection("kubectl", "", shared_kubernetes.ServerFilter)

	serverImage, err := utils.ComputeImage(flags.Image.Name, flags.Image.Tag)
	if err != nil {
		return fmt.Errorf("failed to compute image URL")
	}

	inspectedValues, err := inspect.InspectKubernetes(serverImage, flags.Image.PullPolicy)
	if err != nil {
		return fmt.Errorf("cannot inspect kubernetes values: %s", err)
	}

	err = upgrade_shared.SanityCheck(cnx, inspectedValues, serverImage)
	if err != nil {
		return err
	}

	fqdn, exist := inspectedValues["fqdn"]
	if !exist {
		return fmt.Errorf("inspect function did non return fqdn value")
	}

	clusterInfos, err := shared_kubernetes.CheckCluster()
	if err != nil {
		return err
	}
	kubeconfig := clusterInfos.GetKubeconfig()

	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	defer os.RemoveAll(scriptDir)
	if err != nil {
		return fmt.Errorf("failed to create temporary directory")
	}

	//this is needed because folder with script needs to be mounted
	//check the node before scaling down
	nodeName, err := shared_kubernetes.GetNode("uyuni")
	if err != nil {
		return fmt.Errorf("cannot find node for app uyuni %s", err)
	}

	err = shared_kubernetes.ReplicasTo(shared_kubernetes.ServerFilter, 0)
	if err != nil {
		return fmt.Errorf("cannot set replica to 0: %s", err)
	}

	defer func() {
		// if something is running, we don't need to set replicas to 1
		if _, err = shared_kubernetes.GetNode("uyuni"); err != nil {
			err = shared_kubernetes.ReplicasTo(shared_kubernetes.ServerFilter, 1)
		}
	}()
	if inspectedValues["image_pg_version"] > inspectedValues["current_pg_version"] {
		log.Info().Msgf("Previous postgresql is %s, instead new one is %s. Performing a DB version upgrade...", inspectedValues["current_pg_version"], inspectedValues["image_pg_version"])

		if err := kubernetes.RunPgsqlVersionUpgrade(flags.Image, flags.MigrationImage, nodeName, inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]); err != nil {
			return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err)
		}
	} else if inspectedValues["image_pg_version"] == inspectedValues["current_pg_version"] {
		log.Info().Msgf("Upgrading to %s without changing PostgreSQL version", inspectedValues["uyuni_release"])
	} else {
		return fmt.Errorf("trying to downgrade postgresql from %s to %s", inspectedValues["current_pg_version"], inspectedValues["image_pg_version"])
	}

	schemaUpdateRequired := inspectedValues["current_pg_version"] != inspectedValues["image_pg_version"]
	if err := kubernetes.RunPgsqlFinalizeScript(serverImage, flags.Image.PullPolicy, nodeName, schemaUpdateRequired); err != nil {
		return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err)
	}

	if err := kubernetes.RunPostUpgradeScript(serverImage, flags.Image.PullPolicy, nodeName); err != nil {
		return fmt.Errorf("cannot run post upgrade script: %s", err)
	}

	err = kubernetes.UyuniUpgrade(serverImage, flags.Image.PullPolicy, &flags.Helm, kubeconfig, fqdn, clusterInfos.Ingress)
	if err != nil {
		return fmt.Errorf("cannot upgrade to image %s: %s", serverImage, err)
	}

	return shared_kubernetes.WaitForDeployment(flags.Helm.Uyuni.Namespace, "uyuni", "uyuni")
}
07070100000072000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/upgrade/podman07070100000073000081A4000003E80000006400000001660688CE000006AA000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/upgrade/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/shared"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type podmanUpgradeFlags struct {
	shared.UpgradeFlags `mapstructure:",squash"`
	Podman              podman.PodmanFlags
	MirrorPath          string
}

// NewCommand to upgrade a podman server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	upgradeCmd := &cobra.Command{
		Use:   "podman",
		Short: "upgrade a local server on podman",
		Args:  cobra.RangeArgs(0, 1),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags podmanUpgradeFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, upgradePodman)
		},
	}
	listCmd := &cobra.Command{
		Use:   "list",
		Short: "list available tag for an image",
		Args:  cobra.ExactArgs(0),
		Run: func(cmd *cobra.Command, args []string) {
			viper, _ := utils.ReadConfig(globalFlags.ConfigPath, cmd)

			var flags podmanUpgradeFlags
			if err := viper.Unmarshal(&flags); err != nil {
				log.Fatal().Err(err).Msg("Failed to unmarshall configuration")
			}
			tags, _ := podman.ShowAvailableTag(flags.Image.Name)
			log.Info().Msgf("Available Tags for image: %s", flags.Image.Name)
			for _, value := range tags {
				log.Info().Msgf("%s", value)
			}
		},
	}
	shared.AddUpgradeListFlags(listCmd)
	upgradeCmd.AddCommand(listCmd)

	shared.AddUpgradeFlags(upgradeCmd)
	podman.AddPodmanInstallFlag(upgradeCmd)

	return upgradeCmd
}
07070100000074000081A4000003E80000006400000001660688CE00000B8A000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/upgrade/podman/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"fmt"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/inspect"
	upgrade_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/shared"

	"github.com/uyuni-project/uyuni-tools/mgradm/shared/podman"

	"github.com/uyuni-project/uyuni-tools/shared"
	shared_podman "github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func upgradePodman(globalFlags *types.GlobalFlags, flags *podmanUpgradeFlags, cmd *cobra.Command, args []string) error {
	serverImage, err := utils.ComputeImage(flags.Image.Name, flags.Image.Tag)
	if err != nil {
		return fmt.Errorf("failed to compute image URL")
	}

	inspectedValues, err := inspect.InspectPodman(serverImage, flags.Image.PullPolicy)
	if err != nil {
		return fmt.Errorf("cannot inspect podman values: %s", err)
	}

	cnx := shared.NewConnection("podman", shared_podman.ServerContainerName, "")

	if err := upgrade_shared.SanityCheck(cnx, inspectedValues, serverImage); err != nil {
		return err
	}

	if err := shared_podman.StopService(shared_podman.ServerService); err != nil {
		return fmt.Errorf("cannot stop service %s", err)
	}

	defer func() {
		err = shared_podman.StartService(shared_podman.ServerService)
	}()
	if inspectedValues["image_pg_version"] > inspectedValues["current_pg_version"] {
		log.Info().Msgf("Previous postgresql is %s, instead new one is %s. Performing a DB version upgrade...", inspectedValues["current_pg_version"], inspectedValues["image_pg_version"])
		if err := podman.RunPgsqlVersionUpgrade(flags.Image, flags.MigrationImage, inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]); err != nil {
			return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err)
		}
	} else if inspectedValues["image_pg_version"] == inspectedValues["current_pg_version"] {
		log.Info().Msgf("Upgrading to %s without changing PostgreSQL version", inspectedValues["uyuni_release"])
	} else {
		return fmt.Errorf("trying to downgrade postgresql from %s to %s", inspectedValues["current_pg_version"], inspectedValues["image_pg_version"])
	}

	schemaUpdateRequired := inspectedValues["current_pg_version"] != inspectedValues["image_pg_version"]
	if err := podman.RunPgsqlFinalizeScript(serverImage, schemaUpdateRequired); err != nil {
		return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err)
	}

	if err := podman.RunPostUpgradeScript(serverImage); err != nil {
		return fmt.Errorf("cannot run post upgrade script: %s", err)
	}

	if err := shared_podman.GenerateSystemdConfFile("uyuni-server", "Service", "Environment=UYUNI_IMAGE="+serverImage); err != nil {
		return err
	}
	log.Info().Msg("Waiting for the server to start...")
	return shared_podman.ReloadDaemon(false)
}
07070100000075000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/upgrade/shared07070100000076000081A4000003E80000006400000001660688CE000002EF000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/upgrade/shared/flags.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package shared

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// UpgradeFlags represents flags used for upgrading a server.
type UpgradeFlags struct {
	Image          types.ImageFlags `mapstructure:",squash"`
	MigrationImage types.ImageFlags `mapstructure:"migration"`
}

// AddUpgradeFlags add upgrade flags to a command.
func AddUpgradeFlags(cmd *cobra.Command) {
	utils.AddImageFlag(cmd)
	utils.AddMigrationImageFlag(cmd)
}

// AddUpgradeListFlags add upgrade list flags to a command.
func AddUpgradeListFlags(cmd *cobra.Command) {
	utils.AddImageFlag(cmd)
}
07070100000077000081A4000003E80000006400000001660688CE00001084000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/upgrade/shared/shared.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package shared

import (
	"errors"
	"fmt"
	"regexp"
	"strconv"
	"strings"

	"github.com/rs/zerolog/log"

	"github.com/uyuni-project/uyuni-tools/shared"
)

// CompareVersion compare the server image version and the server deployed  version.
func CompareVersion(imageVersion string, deployedVersion string) int {
	re := regexp.MustCompile(`\((.*?)\)`)
	imageVersionCleaned := strings.ReplaceAll(imageVersion, ".", "")
	imageVersionCleaned = strings.TrimSpace(imageVersionCleaned)
	imageVersionCleaned = re.ReplaceAllString(imageVersionCleaned, "")
	imageVersionInt, _ := strconv.Atoi(imageVersionCleaned)

	deployedVersionCleaned := strings.ReplaceAll(deployedVersion, ".", "")
	deployedVersionCleaned = strings.TrimSpace(deployedVersionCleaned)
	deployedVersionCleaned = re.ReplaceAllString(deployedVersionCleaned, "")
	deployedVersionInt, _ := strconv.Atoi(deployedVersionCleaned)
	return imageVersionInt - deployedVersionInt
}

func isUyuni(cnx *shared.Connection) (bool, error) {
	cnx_args := []string{"/etc/uyuni-release"}
	_, err := cnx.Exec("cat", cnx_args...)
	if err != nil {
		cnx_args := []string{"/etc/susemanager-release"}
		_, err := cnx.Exec("cat", cnx_args...)
		if err != nil {
			return false, errors.New("cannot find neither /etc/uyuni-release nor /etc/susemanagere-release")
		}
		return false, nil
	}
	return true, nil
}

// SanityCheck verifies if an upgrade can be run.
func SanityCheck(cnx *shared.Connection, inspectedValues map[string]string, serverImage string) error {
	isUyuni, err := isUyuni(cnx)
	if err != nil {
		return fmt.Errorf("cannot check server release: %s", err)
	}
	_, isCurrentUyuni := inspectedValues["uyuni_release"]
	_, isCurrentSuma := inspectedValues["suse_manager_release"]

	if isUyuni && isCurrentSuma {
		return fmt.Errorf("currently SUSE Manager %s is installed, instead the image is Uyuni. Upgrade is not supported", inspectedValues["suse_manager_release"])
	}

	if !isUyuni && isCurrentUyuni {
		return fmt.Errorf("currently Uyuni %s is installed, instead the image is SUSE Manager. Upgrade is not supported", inspectedValues["uyuni_release"])
	}

	if isUyuni {
		cnx_args := []string{"s/Uyuni release //g", "/etc/uyuni-release"}
		current_uyuni_release, err := cnx.Exec("sed", cnx_args...)
		if err != nil {
			return fmt.Errorf("failed to read current uyuni release: %s", err)
		}
		log.Debug().Msgf("Current release is %s", string(current_uyuni_release))
		if (len(inspectedValues["uyuni_release"])) <= 0 {
			return fmt.Errorf("cannot fetch release from image %s", serverImage)
		}
		log.Debug().Msgf("Image %s is %s", serverImage, inspectedValues["uyuni_release"])
		if CompareVersion(inspectedValues["uyuni_release"], string(current_uyuni_release)) < 0 {
			return fmt.Errorf("cannot downgrade from version %s to %s", string(current_uyuni_release), inspectedValues["uyuni_release"])
		}
	} else {
		cnx_args := []string{"s/SUSE Manager release //g", "/etc/susemanager-release"}
		current_suse_manager_release, err := cnx.Exec("sed", cnx_args...)
		if err != nil {
			return fmt.Errorf("failed to read current susemanager release: %s", err)
		}
		log.Debug().Msgf("Current release is %s", string(current_suse_manager_release))
		if (len(inspectedValues["suse_manager_release"])) <= 0 {
			return fmt.Errorf("cannot fetch release from image %s", serverImage)
		}
		log.Debug().Msgf("Image %s is %s", serverImage, inspectedValues["suse_manager_release"])
		if CompareVersion(inspectedValues["suse_manager_release"], string(current_suse_manager_release)) < 0 {
			return fmt.Errorf("cannot downgrade from version %s to %s", string(current_suse_manager_release), inspectedValues["suse_manager_release"])
		}
	}

	if (len(inspectedValues["image_pg_version"])) <= 0 {
		return fmt.Errorf("cannot fetch postgresql version from %s", serverImage)
	}
	log.Debug().Msgf("Image %s has PostgreSQL %s", serverImage, inspectedValues["image_pg_version"])
	if (len(inspectedValues["current_pg_version"])) <= 0 {
		return fmt.Errorf("posgresql is not installed in the current deployment")
	}
	log.Debug().Msgf("Current deployment has PostgreSQL %s", inspectedValues["current_pg_version"])

	return nil
}
07070100000078000081A4000003E80000006400000001660688CE000002FC000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/upgrade/upgrade.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0
package upgrade

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/kubernetes"
	"github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// NewCommand for upgrading a local server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	upgradeCmd := &cobra.Command{
		Use:   "upgrade server",
		Short: "upgrade local server",
		Long:  "Upgrade local server",
	}

	upgradeCmd.AddCommand(podman.NewCommand(globalFlags))

	if kubernetesCmd := kubernetes.NewCommand(globalFlags); kubernetesCmd != nil {
		upgradeCmd.AddCommand(kubernetesCmd)
	}

	return upgradeCmd
}
07070100000079000081A4000003E80000006400000001660688CE00000181000000000000000000000000000000000000001B00000000uyuni-tools/mgradm/main.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package main

import (
	"os"

	"github.com/uyuni-project/uyuni-tools/mgradm/cmd"
)

// Run runs the `mgradm` root command.
func Run() error {
	run, err := cmd.NewUyuniadmCommand()
	if err != nil {
		return err
	}
	return run.Execute()
}

func main() {
	if err := Run(); err != nil {
		os.Exit(1)
	}
}
0707010000007A000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001A00000000uyuni-tools/mgradm/shared0707010000007B000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002500000000uyuni-tools/mgradm/shared/kubernetes0707010000007C000081A4000003E80000006400000001660688CE000017A0000000000000000000000000000000000000003500000000uyuni-tools/mgradm/shared/kubernetes/certificates.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"encoding/base64"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/templates"
	cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func installTlsSecret(namespace string, serverCrt []byte, serverKey []byte, rootCaCrt []byte) {
	crdsDir, err := os.MkdirTemp("", "mgradm-*")
	if err != nil {
		log.Fatal().Err(err).Msgf("Failed to create temporary directory")
	}
	defer os.RemoveAll(crdsDir)

	secretPath := filepath.Join(crdsDir, "secret.yaml")
	log.Info().Msg("Creating SSL server certificate secret")
	tlsSecretData := templates.TlsSecretTemplateData{
		Namespace:   namespace,
		Name:        "uyuni-cert",
		Certificate: base64.StdEncoding.EncodeToString(serverCrt),
		Key:         base64.StdEncoding.EncodeToString(serverKey),
		RootCa:      base64.StdEncoding.EncodeToString(rootCaCrt),
	}

	if err = utils.WriteTemplateToFile(tlsSecretData, secretPath, 0500, true); err != nil {
		log.Fatal().Err(err).Msg("Failed to generate uyuni-crt secret definition")
	}
	err = utils.RunCmd("kubectl", "apply", "-f", secretPath)
	if err != nil {
		log.Fatal().Err(err).Msg("Failed to create uyuni-crt TLS secret")
	}

	createCaConfig(rootCaCrt)
}

// Install cert-manager and its CRDs using helm in the cert-manager namespace if needed
// and then create a self-signed CA and issuers.
// Returns helm arguments to be added to use the issuer.
func installSslIssuers(helmFlags *cmd_utils.HelmFlags, sslFlags *cmd_utils.SslCertFlags, rootCa string,
	tlsCert *ssl.SslPair, kubeconfig, fqdn string, imagePullPolicy string) ([]string, error) {
	// Install cert-manager if needed
	if err := installCertManager(helmFlags, kubeconfig, imagePullPolicy); err != nil {
		return []string{}, fmt.Errorf("cannot install cert manager: %s", err)
	}

	log.Info().Msg("Creating SSL certificate issuer")
	crdsDir, err := os.MkdirTemp("", "mgradm-*")
	if err != nil {
		return []string{}, fmt.Errorf("failed to create temporary directory: %s", err)
	}
	defer os.RemoveAll(crdsDir)

	issuerPath := filepath.Join(crdsDir, "issuer.yaml")

	issuerData := templates.IssuerTemplateData{
		Namespace:   helmFlags.Uyuni.Namespace,
		Country:     sslFlags.Country,
		State:       sslFlags.State,
		City:        sslFlags.City,
		Org:         sslFlags.Org,
		OrgUnit:     sslFlags.OU,
		Email:       sslFlags.Email,
		Fqdn:        fqdn,
		RootCa:      rootCa,
		Key:         tlsCert.Key,
		Certificate: tlsCert.Cert,
	}

	if err = utils.WriteTemplateToFile(issuerData, issuerPath, 0500, true); err != nil {
		return []string{}, fmt.Errorf("failed to generate issuer definition: %s", err)
	}

	err = utils.RunCmd("kubectl", "apply", "-f", issuerPath)
	if err != nil {
		log.Fatal().Err(err).Msg("Failed to create issuer")
	}

	// Wait for issuer to be ready
	for i := 0; i < 60; i++ {
		out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "-o=jsonpath={.status.conditions[*].type}",
			"issuer", "uyuni-ca-issuer")
		if err == nil && string(out) == "Ready" {
			return []string{"--set-json", "ingressSslAnnotations={\"cert-manager.io/issuer\": \"uyuni-ca-issuer\"}"}, nil
		}
		time.Sleep(1 * time.Second)
	}
	log.Fatal().Msg("Issuer didn't turn ready after 60s")
	return []string{}, nil
}

func installCertManager(helmFlags *cmd_utils.HelmFlags, kubeconfig string, imagePullPolicy string) error {
	if !kubernetes.IsDeploymentReady("", "cert-manager") {
		log.Info().Msg("Installing cert-manager")
		repo := ""
		chart := helmFlags.CertManager.Chart
		version := helmFlags.CertManager.Version
		namespace := helmFlags.CertManager.Namespace

		args := []string{
			"--set", "installCRDs=true",
			"--set-json", "global.commonLabels={\"installedby\": \"mgradm\"}",
			"--set", "images.pullPolicy=" + kubernetes.GetPullPolicy(imagePullPolicy),
		}
		extraValues := helmFlags.CertManager.Values
		if extraValues != "" {
			args = append(args, "-f", extraValues)
		}

		// Use upstream chart if nothing defined
		if chart == "" {
			repo = "https://charts.jetstack.io"
			chart = "cert-manager"
		}
		// The installedby label will be used to only uninstall what we installed
		if err := kubernetes.HelmUpgrade(kubeconfig, namespace, true, repo, "cert-manager", chart, version, args...); err != nil {
			return fmt.Errorf("cannot run helm upgrade: %s", err)
		}
	}

	// Wait for cert-manager to be ready
	err := kubernetes.WaitForDeployment("", "cert-manager-webhook", "webhook")
	if err != nil {
		return fmt.Errorf("cannot deploy: %s", err)
	}

	return nil
}

func extractCaCertToConfig() {
	// TODO Replace with [trust-manager](https://cert-manager.io/docs/projects/trust-manager/) to automate this
	const jsonPath = "-o=jsonpath={.data.ca\\.crt}"

	log.Info().Msg("Extracting CA certificate to a configmap")
	// Skip extracting if the configmap is already present
	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "configmap", "uyuni-ca", jsonPath)
	log.Info().Msgf("CA cert: %s", string(out))
	if err == nil && len(out) > 0 {
		log.Info().Msg("uyuni-ca configmap already existing, skipping extraction")
		return
	}

	out, err = utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "secret", "uyuni-ca", jsonPath)
	if err != nil {
		log.Fatal().Err(err).Msgf("Failed to get uyuni-ca certificate")
	}

	decoded, err := base64.StdEncoding.DecodeString(string(out))
	if err != nil {
		log.Fatal().Err(err).Msgf("Failed to base64 decode CA certificate")
	}

	createCaConfig(decoded)
}

func createCaConfig(ca []byte) {
	valueArg := "--from-literal=ca.crt=" + string(ca)
	if err := utils.RunCmd("kubectl", "create", "configmap", "uyuni-ca", valueArg); err != nil {
		log.Fatal().Err(err).Msg("Failed to create uyuni-ca config map from certificate")
	}
}
0707010000007D000081A4000003E80000006400000001660688CE000010F2000000000000000000000000000000000000003000000000uyuni-tools/mgradm/shared/kubernetes/install.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"fmt"

	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl"
	cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// HELM_APP_NAME is the Helm application name.
const HELM_APP_NAME = "uyuni"

// Deploy execute a deploy of a given image and helm to a cluster.
func Deploy(cnx *shared.Connection, imageFlags *types.ImageFlags,
	helmFlags *cmd_utils.HelmFlags, sslFlags *cmd_utils.SslCertFlags, clusterInfos *kubernetes.ClusterInfos,
	fqdn string, debug bool, helmArgs ...string) error {
	// If installing on k3s, install the traefik helm config in manifests
	isK3s := clusterInfos.IsK3s()
	IsRke2 := clusterInfos.IsRke2()
	if isK3s {
		InstallK3sTraefikConfig(debug)
	} else if IsRke2 {
		kubernetes.InstallRke2NginxConfig(utils.TCP_PORTS, utils.UDP_PORTS, helmFlags.Uyuni.Namespace)
	}

	serverImage, err := utils.ComputeImage(imageFlags.Name, imageFlags.Tag)
	if err != nil {
		return fmt.Errorf("failed to compute image URL")
	}

	// Install the uyuni server helm chart
	err = UyuniUpgrade(serverImage, imageFlags.PullPolicy, helmFlags, clusterInfos.GetKubeconfig(), fqdn, clusterInfos.Ingress, helmArgs...)
	if err != nil {
		return fmt.Errorf("cannot upgrade: %s", err)
	}

	// Wait for the pod to be started
	err = kubernetes.WaitForDeployment(helmFlags.Uyuni.Namespace, HELM_APP_NAME, "uyuni")
	if err != nil {
		return fmt.Errorf("cannot deploy: %s", err)
	}
	return cnx.WaitForServer()
}

// DeployCertificate executre a deploy a new certificate given an helm.
func DeployCertificate(helmFlags *cmd_utils.HelmFlags, sslFlags *cmd_utils.SslCertFlags, rootCa string,
	ca *ssl.SslPair, kubeconfig string, fqdn string, imagePullPolicy string) ([]string, error) {
	helmArgs := []string{}
	if sslFlags.UseExisting() {
		DeployExistingCertificate(helmFlags, sslFlags, kubeconfig)
	} else {
		// Install cert-manager and a self-signed issuer ready for use
		issuerArgs, err := installSslIssuers(helmFlags, sslFlags, rootCa, ca, kubeconfig, fqdn, imagePullPolicy)
		if err != nil {
			return []string{}, fmt.Errorf("cannot install cert-manager and self-sign issuer: %s", err)
		}
		helmArgs = append(helmArgs, issuerArgs...)

		// Extract the CA cert into uyuni-ca config map as the container shouldn't have the CA secret
		extractCaCertToConfig()
	}

	return helmArgs, nil
}

// DeployExistingCertificate execute a deploy of an existing certificate.
func DeployExistingCertificate(helmFlags *cmd_utils.HelmFlags, sslFlags *cmd_utils.SslCertFlags, kubeconfig string) {
	// Deploy the SSL Certificate secret and CA configmap
	serverCrt, rootCaCrt := ssl.OrderCas(&sslFlags.Ca, &sslFlags.Server)
	serverKey := utils.ReadFile(sslFlags.Server.Key)
	installTlsSecret(helmFlags.Uyuni.Namespace, serverCrt, serverKey, rootCaCrt)

	// Extract the CA cert into uyuni-ca config map as the container shouldn't have the CA secret
	extractCaCertToConfig()
}

// UyuniUpgrade runs an helm upgrade using images and helm configuration as parameters.
func UyuniUpgrade(serverImage string, pullPolicy string, helmFlags *cmd_utils.HelmFlags, kubeconfig string,
	fqdn string, ingress string, helmArgs ...string) error {
	log.Info().Msg("Installing Uyuni")

	// The guessed ingress is passed before the user's value to let the user override it in case we got it wrong.
	helmParams := []string{
		"--set", "ingress=" + ingress,
	}

	extraValues := helmFlags.Uyuni.Values
	if extraValues != "" {
		helmParams = append(helmParams, "-f", extraValues)
	}

	// The values computed from the command line need to be last to override what could be in the extras
	helmParams = append(helmParams,
		"--set", "images.server="+serverImage,
		"--set", "pullPolicy="+kubernetes.GetPullPolicy(pullPolicy),
		"--set", "fqdn="+fqdn)

	helmParams = append(helmParams, helmArgs...)

	namespace := helmFlags.Uyuni.Namespace
	chart := helmFlags.Uyuni.Chart
	version := helmFlags.Uyuni.Version
	return kubernetes.HelmUpgrade(kubeconfig, namespace, true, "", HELM_APP_NAME, chart, version, helmParams...)
}
0707010000007E000081A4000003E80000006400000001660688CE00001C43000000000000000000000000000000000000002C00000000uyuni-tools/mgradm/shared/kubernetes/k3s.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"fmt"
	"os"

	"github.com/rs/zerolog/log"
	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// InstallK3sTraefikConfig installs the K3s Traefik configuration.
func InstallK3sTraefikConfig(debug bool) {
	tcpPorts := []types.PortMap{}
	tcpPorts = append(tcpPorts, utils.TCP_PORTS...)
	if debug {
		tcpPorts = append(tcpPorts, utils.DEBUG_PORTS...)
	}

	kubernetes.InstallK3sTraefikConfig(tcpPorts, utils.UDP_PORTS)
}

// RunPgsqlVersionUpgrade perform a PostgreSQL major upgrade.
func RunPgsqlVersionUpgrade(image types.ImageFlags, migrationImage types.ImageFlags, nodeName string, oldPgsql string, newPgsql string) error {
	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	defer os.RemoveAll(scriptDir)
	if err != nil {
		return fmt.Errorf("failed to create temporary directory")
	}
	if newPgsql > oldPgsql {
		log.Info().Msgf("Previous postgresql is %s, instead new one is %s. Performing a DB version upgrade...", oldPgsql, newPgsql)

		pgsqlVersionUpgradeContainer := "uyuni-upgrade-pgsql"

		migrationImageUrl := ""
		if migrationImage.Name == "" {
			migrationImageUrl, err = utils.ComputeImage(image.Name, image.Tag, fmt.Sprintf("-migration-%s-%s", oldPgsql, newPgsql))
			if err != nil {
				return fmt.Errorf("failed to compute image URL %s", err)
			}
		} else {
			migrationImageUrl, err = utils.ComputeImage(migrationImage.Name, image.Tag)
			if err != nil {
				return fmt.Errorf("failed to compute image URL %s", err)
			}
		}

		log.Info().Msgf("Using migration image %s", migrationImageUrl)
		pgsqlVersionUpgradeScriptName, err := adm_utils.GeneratePgsqlVersionUpgradeScript(scriptDir, oldPgsql, newPgsql, true)
		if err != nil {
			return fmt.Errorf("cannot generate postgresql database version upgrade script: %s", err)
		}

		//delete pending pod and then check the node, because in presence of more than a pod GetNode return is wrong
		if err := kubernetes.DeletePod(pgsqlVersionUpgradeContainer, kubernetes.ServerFilter); err != nil {
			return fmt.Errorf("cannot delete %s: %s", pgsqlVersionUpgradeContainer, err)
		}

		//generate deploy data
		pgsqlVersioUpgradeDeployData := types.Deployment{
			APIVersion: "v1",
			Spec: &types.Spec{
				RestartPolicy: "Never",
				NodeName:      nodeName,
				Containers: []types.Container{
					{
						Name: pgsqlVersionUpgradeContainer,
						VolumeMounts: append(utils.PgsqlRequiredVolumeMounts,
							types.VolumeMount{MountPath: "/var/lib/uyuni-tools", Name: "var-lib-uyuni-tools"}),
					},
				},
				Volumes: append(utils.PgsqlRequiredVolumes,
					types.Volume{Name: "var-lib-uyuni-tools", HostPath: &types.HostPath{Path: scriptDir, Type: "Directory"}}),
			},
		}

		//transform deploy in JSON
		overridePgsqlVersioUpgrade, err := kubernetes.GenerateOverrideDeployment(pgsqlVersioUpgradeDeployData)
		if err != nil {
			return err
		}

		err = kubernetes.RunPod(pgsqlVersionUpgradeContainer, kubernetes.ServerFilter, migrationImageUrl, image.PullPolicy, "/var/lib/uyuni-tools/"+pgsqlVersionUpgradeScriptName, overridePgsqlVersioUpgrade)
		if err != nil {
			return fmt.Errorf("error running container %s: %s", pgsqlVersionUpgradeContainer, err)
		}
	}
	return nil
}

// RunPgsqlFinalizeScript run the script with all the action required to a db after upgrade.
func RunPgsqlFinalizeScript(serverImage string, pullPolicy string, nodeName string, schemaUpdateRequired bool) error {
	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	defer os.RemoveAll(scriptDir)
	if err != nil {
		return fmt.Errorf("failed to create temporary directory")
	}
	pgsqlFinalizeContainer := "uyuni-finalize-pgsql"
	pgsqlFinalizeScriptName, err := adm_utils.GenerateFinalizePostgresScript(scriptDir, true, schemaUpdateRequired, true, true, true)
	if err != nil {
		return fmt.Errorf("cannot generate postgresql finalization script %s", err)
	}
	//delete pending pod and then check the node, because in presence of more than a pod GetNode return is wrong
	if err := kubernetes.DeletePod(pgsqlFinalizeContainer, kubernetes.ServerFilter); err != nil {
		return fmt.Errorf("cannot delete %s: %s", pgsqlFinalizeContainer, err)
	}
	//generate deploy data
	pgsqlFinalizeDeployData := types.Deployment{
		APIVersion: "v1",
		Spec: &types.Spec{
			RestartPolicy: "Never",
			NodeName:      nodeName,
			Containers: []types.Container{
				{
					Name: pgsqlFinalizeContainer,
					VolumeMounts: append(utils.PgsqlRequiredVolumeMounts,
						types.VolumeMount{MountPath: "/var/lib/uyuni-tools", Name: "var-lib-uyuni-tools"}),
				},
			},
			Volumes: append(utils.PgsqlRequiredVolumes,
				types.Volume{Name: "var-lib-uyuni-tools", HostPath: &types.HostPath{Path: scriptDir, Type: "Directory"}}),
		},
	}
	//transform deploy data in JSON
	overridePgsqlFinalize, err := kubernetes.GenerateOverrideDeployment(pgsqlFinalizeDeployData)
	if err != nil {
		return err
	}
	err = kubernetes.RunPod(pgsqlFinalizeContainer, kubernetes.ServerFilter, serverImage, pullPolicy, "/var/lib/uyuni-tools/"+pgsqlFinalizeScriptName, overridePgsqlFinalize)
	if err != nil {
		return fmt.Errorf("error running container %s: %s", pgsqlFinalizeContainer, err)
	}
	return nil
}

// RunPostUpgradeScript run the script with the changes to apply after the upgrade.
func RunPostUpgradeScript(serverImage string, pullPolicy string, nodeName string) error {
	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	defer os.RemoveAll(scriptDir)
	if err != nil {
		return fmt.Errorf("failed to create temporary directory")
	}
	postUpgradeContainer := "uyuni-post-upgrade"
	postUpgradeScriptName, err := adm_utils.GeneratePostUpgradeScript(scriptDir, "localhost")
	if err != nil {
		return fmt.Errorf("cannot generate postgresql finalization script %s", err)
	}

	//delete pending pod and then check the node, because in presence of more than a pod GetNode return is wrong
	if err := kubernetes.DeletePod(postUpgradeContainer, kubernetes.ServerFilter); err != nil {
		return fmt.Errorf("cannot delete %s: %s", postUpgradeContainer, err)
	}
	//generate deploy data
	postUpgradeDeployData := types.Deployment{
		APIVersion: "v1",
		Spec: &types.Spec{
			RestartPolicy: "Never",
			NodeName:      nodeName,
			Containers: []types.Container{
				{
					Name: postUpgradeContainer,
					VolumeMounts: append(utils.PgsqlRequiredVolumeMounts,
						types.VolumeMount{MountPath: "/var/lib/uyuni-tools", Name: "var-lib-uyuni-tools"}),
				},
			},
			Volumes: append(utils.PgsqlRequiredVolumes,
				types.Volume{Name: "var-lib-uyuni-tools", HostPath: &types.HostPath{Path: scriptDir, Type: "Directory"}}),
		},
	}
	//transform deploy data in JSON
	overridePostUpgrade, err := kubernetes.GenerateOverrideDeployment(postUpgradeDeployData)
	if err != nil {
		return err
	}

	err = kubernetes.RunPod(postUpgradeContainer, kubernetes.ServerFilter, serverImage, pullPolicy, "/var/lib/uyuni-tools/"+postUpgradeScriptName, overridePostUpgrade)
	if err != nil {
		return fmt.Errorf("error running container %s: %s", postUpgradeContainer, err)
	}
	return nil
}
0707010000007F000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002100000000uyuni-tools/mgradm/shared/podman07070100000080000081A4000003E80000006400000001660688CE00002C5E000000000000000000000000000000000000002B00000000uyuni-tools/mgradm/shared/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/templates"
	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

const commonArgs = "--rm --cap-add NET_RAW --tmpfs /run -v cgroup:/sys/fs/cgroup:rw"

// GetCommonParams splits the common arguments.
func GetCommonParams() []string {
	return strings.Split(commonArgs, " ")
}

// GetExposedPorts returns the port exposed.
func GetExposedPorts(debug bool) []types.PortMap {
	ports := []types.PortMap{
		utils.NewPortMap("https", 443, 443),
		utils.NewPortMap("http", 80, 80),
	}
	ports = append(ports, utils.TCP_PORTS...)
	ports = append(ports, utils.UDP_PORTS...)

	if debug {
		ports = append(ports, utils.DEBUG_PORTS...)
	}

	return ports
}

// GenerateSystemdService creates a serverY systemd file.
func GenerateSystemdService(tz string, image string, debug bool, podmanArgs []string) error {
	if err := podman.SetupNetwork(); err != nil {
		return fmt.Errorf("cannot setup network: %s", err)
	}

	log.Info().Msg("Enabling system service")
	data := templates.PodmanServiceTemplateData{
		Volumes:    utils.ServerVolumeMounts,
		NamePrefix: "uyuni",
		Args:       commonArgs + " " + strings.Join(podmanArgs, " "),
		Ports:      GetExposedPorts(debug),
		Timezone:   tz,
		Network:    podman.UyuniNetwork,
	}
	if err := utils.WriteTemplateToFile(data, podman.GetServicePath("uyuni-server"), 0555, false); err != nil {
		return fmt.Errorf("failed to generate systemd service unit file: %s", err)
	}

	if err := podman.GenerateSystemdConfFile("uyuni-server", "Service", "Environment=UYUNI_IMAGE="+image); err != nil {
		return fmt.Errorf("cannot generate systemd conf file: %s", err)
	}
	return podman.ReloadDaemon(false)
}

// UpdateSslCertificate update SSL certificate.
func UpdateSslCertificate(cnx *shared.Connection, chain *ssl.CaChain, serverPair *ssl.SslPair) error {
	ssl.CheckPaths(chain, serverPair)

	// Copy the CAs, certificate and key to the container
	const certDir = "/tmp/uyuni-tools"
	if err := utils.RunCmd("podman", "exec", podman.ServerContainerName, "mkdir", "-p", certDir); err != nil {
		return fmt.Errorf("failed to create temporary folder on container to copy certificates to")
	}

	rootCaPath := path.Join(certDir, "root-ca.crt")
	serverCrtPath := path.Join(certDir, "server.crt")
	serverKeyPath := path.Join(certDir, "server.key")

	log.Debug().Msgf("Intermediate CA flags: %v", chain.Intermediate)

	args := []string{
		"exec",
		podman.ServerContainerName,
		"mgr-ssl-cert-setup",
		"-vvv",
		"--root-ca-file", rootCaPath,
		"--server-cert-file", serverCrtPath,
		"--server-key-file", serverKeyPath,
	}

	if err := cnx.Copy(chain.Root, "server:"+rootCaPath, "root", "root"); err != nil {
		return fmt.Errorf("cannot copy %s: %s", rootCaPath, err)
	}
	if err := cnx.Copy(serverPair.Cert, "server:"+serverCrtPath, "root", "root"); err != nil {
		return fmt.Errorf("cannot copy %s: %s", serverCrtPath, err)
	}
	if err := cnx.Copy(serverPair.Key, "server:"+serverKeyPath, "root", "root"); err != nil {
		return fmt.Errorf("cannot copy %s: %s", serverKeyPath, err)
	}

	for i, ca := range chain.Intermediate {
		caFilename := fmt.Sprintf("ca-%d.crt", i)
		caPath := path.Join(certDir, caFilename)
		args = append(args, "--intermediate-ca-file", caPath)
		if err := cnx.Copy(ca, "server:"+caPath, "root", "root"); err != nil {
			return fmt.Errorf("cannot copy %s: %s", caPath, err)
		}
	}

	// Check and install then using mgr-ssl-cert-setup
	if _, err := utils.RunCmdOutput(zerolog.InfoLevel, "podman", args...); err != nil {
		return fmt.Errorf("failed to update SSL certificate")
	}

	// Clean the copied files and the now useless ssl-build
	if err := utils.RunCmd("podman", "exec", podman.ServerContainerName, "rm", "-rf", certDir); err != nil {
		return fmt.Errorf("failed to remove copied certificate files in the container")
	}

	const sslbuildPath = "/root/ssl-build"
	if cnx.TestExistenceInPod(sslbuildPath) {
		if err := utils.RunCmd("podman", "exec", podman.ServerContainerName, "rm", "-rf", sslbuildPath); err != nil {
			return fmt.Errorf("failed to remove now useless ssl-build folder in the container")
		}
	}

	// The services need to be restarted
	log.Info().Msg("Restarting services after updating the certificate")
	return utils.RunCmdStdMapping(zerolog.DebugLevel, "podman", "exec", podman.ServerContainerName, "spacewalk-service", "restart")
}

// RunContainer execute a container.
func RunContainer(name string, image string, extraArgs []string, cmd []string) error {
	podmanArgs := append([]string{"run", "--name", name}, GetCommonParams()...)
	podmanArgs = append(podmanArgs, extraArgs...)
	for _, volume := range utils.ServerVolumeMounts {
		podmanArgs = append(podmanArgs, "-v", volume.Name+":"+volume.MountPath)
	}
	podmanArgs = append(podmanArgs, image)
	podmanArgs = append(podmanArgs, cmd...)

	err := utils.RunCmdStdMapping(zerolog.DebugLevel, "podman", podmanArgs...)
	if err != nil {
		return fmt.Errorf("failed to run %s container: %s", name, err)
	}

	return nil
}

// RunMigration migrate an existing remote server to a container.
func RunMigration(serverImage string, pullPolicy string, sshAuthSocket string, sshConfigPath string, sshKnownhostsPath string, sourceFqdn string) (string, string, string, error) {
	scriptDir, err := adm_utils.GenerateMigrationScript(sourceFqdn, false)
	if err != nil {
		return "", "", "", fmt.Errorf("cannot generate migration script: %s", err)
	}
	defer os.RemoveAll(scriptDir)

	extraArgs := []string{
		"--security-opt", "label:disable",
		"-e", "SSH_AUTH_SOCK",
		"-v", filepath.Dir(sshAuthSocket) + ":" + filepath.Dir(sshAuthSocket),
		"-v", scriptDir + ":/var/lib/uyuni-tools/",
	}

	if sshConfigPath != "" {
		extraArgs = append(extraArgs, "-v", sshConfigPath+":/tmp/ssh_config")
	}

	if sshKnownhostsPath != "" {
		extraArgs = append(extraArgs, "-v", sshKnownhostsPath+":/etc/ssh/ssh_known_hosts")
	}

	inspectedHostValues, err := adm_utils.InspectHost()
	if err != nil {
		return "", "", "", fmt.Errorf("cannot inspect host values: %s", err)
	}

	pullArgs := []string{}
	_, scc_user_exist := inspectedHostValues["host_scc_username"]
	_, scc_user_password := inspectedHostValues["host_scc_password"]
	if scc_user_exist && scc_user_password {
		pullArgs = append(pullArgs, "--creds", inspectedHostValues["host_scc_username"]+":"+inspectedHostValues["host_scc_password"])
	}

	preparedImage, err := podman.PrepareImage(serverImage, pullPolicy, pullArgs...)
	if err != nil {
		return "", "", "", err
	}

	log.Info().Msg("Migrating server")
	if err := RunContainer("uyuni-migration", preparedImage, extraArgs,
		[]string{"/var/lib/uyuni-tools/migrate.sh"}); err != nil {
		return "", "", "", fmt.Errorf("cannot run uyuni migration container: %s", err)
	}
	tz, oldPgVersion, newPgVersion, err := adm_utils.ReadContainerData(scriptDir)

	if err != nil {
		return "", "", "", fmt.Errorf("cannot read extracted data: %s", err)
	}

	return tz, oldPgVersion, newPgVersion, nil
}

// RunPgsqlVersionUpgrade perform a PostgreSQL major upgrade.
func RunPgsqlVersionUpgrade(image types.ImageFlags, migrationImage types.ImageFlags, oldPgsql string, newPgsql string) error {
	log.Info().Msgf("Previous postgresql is %s, instead new one is %s. Performing a DB version upgrade...", oldPgsql, newPgsql)

	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	defer os.RemoveAll(scriptDir)
	if err != nil {
		return fmt.Errorf("failed to create temporary directory")
	}
	if newPgsql > oldPgsql {
		pgsqlVersionUpgradeContainer := "uyuni-upgrade-pgsql"
		extraArgs := []string{
			"-v", scriptDir + ":/var/lib/uyuni-tools/",
			"--security-opt", "label:disable",
		}

		migrationImageUrl := ""
		if migrationImage.Name == "" {
			migrationImageUrl, err = utils.ComputeImage(image.Name, image.Tag, fmt.Sprintf("-migration-%s-%s", oldPgsql, newPgsql))
			if err != nil {
				return fmt.Errorf("failed to compute image URL %s", err)
			}
		} else {
			migrationImageUrl, err = utils.ComputeImage(migrationImage.Name, image.Tag)
			if err != nil {
				return fmt.Errorf("failed to compute image URL %s", err)
			}
		}

		inspectedHostValues, err := adm_utils.InspectHost()
		if err != nil {
			return fmt.Errorf("cannot inspect host values: %s", err)
		}

		pullArgs := []string{}
		_, scc_user_exist := inspectedHostValues["host_scc_username"]
		_, scc_user_password := inspectedHostValues["host_scc_password"]
		if scc_user_exist && scc_user_password {
			pullArgs = append(pullArgs, "--creds", inspectedHostValues["host_scc_username"]+":"+inspectedHostValues["host_scc_password"])
		}

		preparedImage, err := podman.PrepareImage(migrationImageUrl, image.PullPolicy, pullArgs...)
		if err != nil {
			return err
		}

		log.Info().Msgf("Using migration image %s", preparedImage)

		pgsqlVersionUpgradeScriptName, err := adm_utils.GeneratePgsqlVersionUpgradeScript(scriptDir, oldPgsql, newPgsql, false)
		if err != nil {
			return fmt.Errorf("cannot generate postgresql database version upgrade script %s", err)
		}

		err = RunContainer(pgsqlVersionUpgradeContainer, preparedImage, extraArgs,
			[]string{"/var/lib/uyuni-tools/" + pgsqlVersionUpgradeScriptName})
		if err != nil {
			return err
		}
	}
	return nil
}

// RunPgsqlFinalizeScript run the script with all the action required to a db after upgrade.
func RunPgsqlFinalizeScript(serverImage string, schemaUpdateRequired bool) error {
	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	defer os.RemoveAll(scriptDir)
	if err != nil {
		return fmt.Errorf("failed to create temporary directory")
	}

	extraArgs := []string{
		"-v", scriptDir + ":/var/lib/uyuni-tools/",
		"--security-opt", "label:disable",
	}
	pgsqlFinalizeContainer := "uyuni-finalize-pgsql"
	pgsqlFinalizeScriptName, err := adm_utils.GenerateFinalizePostgresScript(scriptDir, true, schemaUpdateRequired, true, true, false)
	if err != nil {
		return fmt.Errorf("cannot generate postgresql finalization script %s", err)
	}
	err = RunContainer(pgsqlFinalizeContainer, serverImage, extraArgs,
		[]string{"/var/lib/uyuni-tools/" + pgsqlFinalizeScriptName})
	if err != nil {
		return err
	}
	return nil
}

// RunPostUpgradeScript run the script with the changes to apply after the upgrade.
func RunPostUpgradeScript(serverImage string) error {
	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	defer os.RemoveAll(scriptDir)
	if err != nil {
		return fmt.Errorf("failed to create temporary directory")
	}
	postUpgradeContainer := "uyuni-post-upgrade"
	extraArgs := []string{
		"-v", scriptDir + ":/var/lib/uyuni-tools/",
		"--security-opt", "label:disable",
	}
	postUpgradeScriptName, err := adm_utils.GeneratePostUpgradeScript(scriptDir, "localhost")
	if err != nil {
		return fmt.Errorf("cannot generate postgresql finalization script %s", err)
	}
	err = RunContainer(postUpgradeContainer, serverImage, extraArgs,
		[]string{"/var/lib/uyuni-tools/" + postUpgradeScriptName})
	if err != nil {
		return err
	}
	return nil
}
07070100000081000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001E00000000uyuni-tools/mgradm/shared/ssl07070100000082000081A4000003E80000006400000001660688CE00001DAA000000000000000000000000000000000000002500000000uyuni-tools/mgradm/shared/ssl/ssl.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package ssl

import (
	"bytes"
	"errors"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// CaChain is a type to store CA Chain.
type CaChain struct {
	Root         string
	Intermediate []string
}

// SslPait is a type for SSL Cert and Key.
type SslPair struct {
	Cert string
	Key  string
}

// Generate the server certificate with the CA chain.
// Returns the certificate chain and the root CA.
func OrderCas(chain *CaChain, serverPair *SslPair) ([]byte, []byte) {
	CheckPaths(chain, serverPair)

	// Extract all certificates and their data
	certs := readCertificates(chain.Root)
	for _, caPath := range chain.Intermediate {
		certs = append(certs, readCertificates(caPath)...)
	}
	serverCerts := readCertificates(serverPair.Cert)
	certs = append(certs, serverCerts...)

	serverCert, err := findServerCert(certs)
	if err != nil {
		log.Fatal().Msg("Failed to find a non-CA certificate")
	}

	// Map all certificates using their hashes
	mapBySubjectHash := map[string]certificate{}
	if serverCert.subjectHash != "" {
		mapBySubjectHash[serverCert.subjectHash] = *serverCert
	}

	for _, caCert := range certs {
		if caCert.subjectHash != "" {
			mapBySubjectHash[caCert.subjectHash] = caCert
		}
	}

	// Sort from server certificate to RootCA
	return sortCertificates(mapBySubjectHash, serverCert.subjectHash)
}

type certificate struct {
	content      []byte
	subject      string
	subjectHash  string
	issuer       string
	issuerHash   string
	startDate    time.Time
	endDate      time.Time
	subjectKeyId string
	authKeyId    string
	isCa         bool
	isRoot       bool
}

func findServerCert(certs []certificate) (*certificate, error) {
	for _, cert := range certs {
		if !cert.isCa {
			return &cert, nil
		}
	}
	return nil, errors.New("expected to find a certificate, got none")
}

func readCertificates(path string) []certificate {
	fd, err := os.Open(path)
	if err != nil {
		log.Fatal().Err(err).Msgf("Failed to read certificate file %s", path)
	}

	certs := []certificate{}
	for {
		log.Debug().Msgf("Running openssl x509 on %s", path)
		cmd := exec.Command("openssl", "x509")
		cmd.Stdin = fd
		out, err := cmd.Output()

		if err != nil {
			// openssl got an invalid certificate or the end of the file
			break
		}

		// Extract data from the certificate
		cert := extractCertificateData(out)
		certs = append(certs, cert)
	}
	return certs
}

// Extract data from the certificate to help ordering and verifying it.
func extractCertificateData(content []byte) certificate {
	args := []string{"x509", "-noout", "-subject", "-subject_hash", "-startdate", "-enddate",
		"-issuer", "-issuer_hash", "-ext", "subjectKeyIdentifier,authorityKeyIdentifier,basicConstraints"}
	log.Debug().Msg("Running command openssl " + strings.Join(args, " "))
	cmd := exec.Command("openssl", args...)

	log.Trace().Msgf("Extracting data from certificate:\n%s", string(content))

	reader := bytes.NewReader(content)
	cmd.Stdin = reader

	out, err := cmd.Output()
	if err != nil {
		log.Fatal().Err(err).Msg("Failed to extract data from certificate")
	}
	lines := strings.Split(string(out), "\n")

	cert := certificate{content: content}

	const timeLayout = "Jan 2 15:04:05 2006 MST"

	nextVal := ""
	for _, line := range lines {
		if strings.TrimSpace(line) == "" {
			continue
		}
		if strings.HasPrefix(line, "subject=") {
			cert.subject = strings.SplitN(line, "=", 2)[1]
		} else if strings.HasPrefix(line, "issuer=") {
			cert.issuer = strings.SplitN(line, "=", 2)[1]
		} else if strings.HasPrefix(line, "notBefore=") {
			date := strings.SplitN(line, "=", 2)[1]
			cert.startDate, err = time.Parse(timeLayout, date)
			if err != nil {
				log.Fatal().Err(err).Msgf("Failed to parse start date: %s\n", date)
			}
		} else if strings.HasPrefix(line, "notAfter=") {
			date := strings.SplitN(line, "=", 2)[1]
			cert.endDate, err = time.Parse(timeLayout, date)
			if err != nil {
				log.Fatal().Err(err).Msgf("Failed to parse end date: %s\n", date)
			}
		} else if strings.HasPrefix(line, "X509v3 Subject Key Identifier") {
			nextVal = "subjectKeyId"
		} else if strings.HasPrefix(line, "X509v3 Authority Key Identifier") {
			nextVal = "authKeyId"
		} else if strings.HasPrefix(line, "X509v3 Basic Constraints") {
			nextVal = "basicConstraints"
		} else if strings.HasPrefix(line, "    ") {
			if nextVal == "subjectKeyId" {
				cert.subjectKeyId = strings.ToUpper(strings.TrimSpace(line))
			} else if nextVal == "authKeyId" && strings.HasPrefix(line, "    keyid:") {
				cert.authKeyId = strings.ToUpper(strings.TrimSpace(strings.SplitN(line, ":", 2)[1]))
			} else if nextVal == "basicConstraints" && strings.Contains(line, "CA:TRUE") {
				cert.isCa = true
			} else {
				// Unhandled extension value
				continue
			}
		} else if cert.subjectHash == "" {
			// subject_hash comes first without key to identify it
			cert.subjectHash = strings.TrimSpace(line)
		} else {
			// second issue_hash without key to identify this value
			cert.issuerHash = strings.TrimSpace(line)
		}
	}

	if cert.subject == cert.issuer {
		cert.isRoot = true
		// Some Root CAs might not have their authorityKeyIdentifier set to themself
		if cert.isCa && cert.authKeyId == "" {
			cert.authKeyId = cert.subjectKeyId
		}
	} else {
		cert.isRoot = false
	}
	return cert
}

// Prepare the certificate chain starting by the server up to the root CA.
// Returns the certificate chain and the root CA.
func sortCertificates(mapBySubjectHash map[string]certificate, serverCertHash string) ([]byte, []byte) {
	if len(mapBySubjectHash) == 0 {
		log.Fatal().Msg("No CA found in hash")
	}

	cert := mapBySubjectHash[serverCertHash]
	issuerHash := cert.issuerHash
	_, found := mapBySubjectHash[issuerHash]
	if issuerHash == "" || !found {
		log.Fatal().Msg("No CA found for server certificate")
	}

	sortedChain := bytes.NewBuffer(mapBySubjectHash[serverCertHash].content)
	var rootCa []byte

	for {
		cert, found = mapBySubjectHash[issuerHash]
		if !found {
			log.Fatal().Msgf("Missing CA with subject hash %s", issuerHash)
		}

		nextHash := cert.issuerHash
		if nextHash == issuerHash {
			// Found Root CA, we can exit
			rootCa = cert.content
			break
		}
		issuerHash = nextHash
		sortedChain.Write(cert.content)
	}
	return sortedChain.Bytes(), rootCa
}

// Ensures that all the passed path exists and the required files are available.
func CheckPaths(chain *CaChain, serverPair *SslPair) {
	mandatoryFile(chain.Root, "root CA")
	for _, ca := range chain.Intermediate {
		optionalFile(ca)
	}
	mandatoryFile(serverPair.Cert, "server certificate")
	mandatoryFile(serverPair.Key, "server key")
}

func mandatoryFile(file string, msg string) {
	if file == "" {
		log.Fatal().Msgf("%s is required", msg)
	}
	optionalFile(file)
}

func optionalFile(file string) {
	if file != "" && !utils.FileExists(file) {
		log.Fatal().Msgf("%s file is not accessible", file)
	}
}

// Converts an SSL key to RSA.
func GetRsaKey(keyPath string, password string) []byte {
	// Kubernetes only handles RSA private TLS keys, convert and strip password
	caPassword := password
	utils.AskPasswordIfMissing(&caPassword, "Source server SSL CA private key password", 0, 0)

	// Convert the key file to RSA format for kubectl to handle it
	cmd := exec.Command("openssl", "rsa", "-in", keyPath, "-passin", "env:pass")
	cmd.Env = append(cmd.Env, "pass="+caPassword)
	out, err := cmd.Output()
	if err != nil {
		log.Fatal().Err(err).Msg("Failed to convert CA private key to RSA")
	}
	return out
}
07070100000083000081A4000003E80000006400000001660688CE000016C7000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/shared/ssl/ssl_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package ssl

import (
	"strings"
	"testing"
)

func TestReadCertificatesRootCa(t *testing.T) {
	actual := readCertificates("testdata/chain1/root-ca.crt")
	if len(actual) != 1 {
		t.Errorf("readCertificates got %d certificates; want 1", len(actual))
	}

	if !actual[0].isRoot {
		t.Error("CA should be root")
	}
}

func TestReadCertificatesNoCa(t *testing.T) {
	actual := readCertificates("testdata/chain1/server.crt")
	if len(actual) != 1 {
		t.Errorf("readCertificates got %d certificates; want 1", len(actual))
	}

	if actual[0].isCa {
		t.Error("Shouldn't be a CA certificate")
	}
}

func TestReadCertificatesMultiple(t *testing.T) {
	actual := readCertificates("testdata/chain1/intermediate-ca.crt")
	if len(actual) != 2 {
		t.Errorf("readCertificates got %d certificates; want 2", len(actual))
	}

	content := string(actual[0].content)
	if !strings.HasPrefix(content, "-----BEGIN CERTIFICATE-----\nMIIEXjCCA0agA") ||
		!strings.HasSuffix(content, "nrUN5m7Y0taw4qrOVOZRmGXu\n-----END CERTIFICATE-----\n") {
		t.Errorf("Wrong certificate content:\n%s", content)
	}

	if actual[1].subject != "C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = TeamCA" {
		t.Errorf("Wrong certificate subject: %s", actual[1].subject)
	}

	if actual[1].subjectHash != "85a51924" {
		t.Errorf("Wrong subject hash: %s", actual[1].subjectHash)
	}

	if actual[0].issuer != "C = DE, ST = STATE, L = CITY, O = ORG, OU = ORGUNIT, CN = RootCA" {
		t.Errorf("Wrong certificate issuer: %s", actual[0].issuer)
	}

	if actual[0].issuerHash != "e96ab651" {
		t.Errorf("Wrong issuer hash: %s", actual[0].issuerHash)
	}

	if actual[0].isRoot {
		t.Error("CA shouldn't be root")
	}

	if !actual[0].isCa {
		t.Error("Should be a CA")
	}

	if actual[1].subjectKeyId != "62:00:25:E4:EE:70:E5:37:2D:1E:9E:AE:4E:B7:3E:FC:62:08:BF:27" {
		t.Errorf("Wrong subject key id: %s", actual[1].subjectKeyId)
	}

	if actual[0].authKeyId != "6E:6D:4B:35:22:23:3E:13:18:A5:93:61:0E:9C:BE:1E:D2:B8:1B:D4" {
		t.Errorf("Wrong auth key id: %s", actual[0].authKeyId)
	}
}

func TestOrderCas(t *testing.T) {
	chain := CaChain{Root: "testdata/chain1/root-ca.crt", Intermediate: []string{"testdata/chain1/intermediate-ca.crt"}}
	server := SslPair{Cert: "testdata/chain1/server.crt", Key: "testdata/chain1/server.key"}

	certs, rootCa := OrderCas(&chain, &server)
	ordered := strings.Split(string(certs), "-----BEGIN CERTIFICATE-----\n")

	if ordered[0] != "" {
		t.Errorf("Found unknown content before first certificate: %s", ordered[0])
	}
	onlyCerts := ordered[1:]

	expected := []struct {
		Begin string
		End   string
	}{
		{Begin: "MIIEdDCCA1ygAwIBAgIUZ2P1Ka9Eun", End: "JtS8rmkQpYyJciifX0PxYzTg=="},
		{Begin: "MIIETzCCAzegAwIBAgIUZ2P1Ka9Eun", End: "s3DjcCbkzyTUCKh9Po4\nmoUf"},
		{Begin: "MIIEXjCCA0agAwIBAgIUZ2P1Ka9Eunnv3dy/", End: "nrUN5m7Y0taw4qrOVOZRmGXu"},
	}

	// Do not count the empty first item
	if len(onlyCerts) != len(expected) {
		t.Errorf("Wrong number of certificates in the chain: got %d; want %d", len(onlyCerts), len(expected))
	}

	for i, data := range expected {
		if !strings.HasPrefix(onlyCerts[i], data.Begin) ||
			!strings.HasSuffix(onlyCerts[i], data.End+"\n-----END CERTIFICATE-----\n") {
			t.Errorf("Invalid certificate #%d, got:\n:%s", i, onlyCerts[i])
		}
	}

	rootCert := string(rootCa)
	if !strings.HasPrefix(rootCert, "-----BEGIN CERTIFICATE-----\nMIIEVjCCAz6gAwIBAgIUSZYESIXLDe") ||
		!strings.HasSuffix(rootCert, "5c7cfxV\nkABuj9PJxnNnFQ==\n-----END CERTIFICATE-----\n") {
		t.Errorf("Invalid root CA certificate, got:\n:%s", rootCert)
	}
}

func TestFindServerCertificate(t *testing.T) {
	certsList := readCertificates("testdata/chain2/spacewalk.crt")
	actual, err := findServerCert(certsList)

	if err != nil {
		t.Error("Expected to find a certificate, got none")
	}

	if actual.subjectHash != "78b716a6" {
		t.Errorf("Wrong subject hash, got %s", actual.subjectHash)
	}
}

// Test a CA chain with all the chain in the server certificate file.
func TestOrderCasChain2(t *testing.T) {
	chain := CaChain{Root: "testdata/chain2/RHN-ORG-TRUSTED-SSL-CERT", Intermediate: []string{}}
	server := SslPair{Cert: "testdata/chain2/spacewalk.crt", Key: "testdata/chain2/spacewalk.key"}

	certs, rootCa := OrderCas(&chain, &server)
	ordered := strings.Split(string(certs), "-----BEGIN CERTIFICATE-----\n")

	if ordered[0] != "" {
		t.Errorf("Found unknown content before first certificate: %s", ordered[0])
	}
	onlyCerts := ordered[1:]

	expected := []struct {
		Begin string
		End   string
	}{
		{Begin: "MIIEejCCA2KgAwIBAgIUEbWzxg57E", End: "Ur+fgZpBNvbkjD8b+S0ECQA6Dg=="},
		{Begin: "MIIETzCCAzegAwIBAgIUEbWzxg57E", End: "TT2Sljt0YfkmWfdXA\nwOUt"},
		{Begin: "MIIEXjCCA0agAwIBAgIUEbWzxg57E", End: "ivyvRvlwCUNstG6u8Y7IxHHn"},
	}

	// Do not count the empty first item
	if len(onlyCerts) != len(expected) {
		t.Errorf("Wrong number of certificates in the chain: got %d; want %d", len(onlyCerts), len(expected))
	}

	for i, data := range expected {
		if !strings.HasPrefix(onlyCerts[i], data.Begin) ||
			!strings.HasSuffix(onlyCerts[i], data.End+"\n-----END CERTIFICATE-----\n") {
			t.Errorf("Invalid certificate #%d, got:\n:%s", i, onlyCerts[i])
		}
	}

	rootCert := string(rootCa)
	if !strings.HasPrefix(rootCert, "-----BEGIN CERTIFICATE-----\nMIIEVjCCAz6gAwIBAgIUA12e94NK") ||
		!strings.HasSuffix(rootCert, "AQKotV5y5qBInw==\n-----END CERTIFICATE-----\n") {
		t.Errorf("Invalid root CA certificate, got:\n:%s", rootCert)
	}
}

func TestGetRsaKey(t *testing.T) {
	actual := string(GetRsaKey("testdata/RootCA.key", "secret"))
	if !strings.HasPrefix(actual, "-----BEGIN PRIVATE KEY-----\nMIIEugIBADANBgkqhkiG9w0BAQEFAAS") ||
		!strings.HasSuffix(actual, "DKY9SmW6QD+RJwbMc4M=\n-----END PRIVATE KEY-----\n") {
		t.Errorf("Unexpected generated RSA key: %s", actual)
	}
}
07070100000084000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002700000000uyuni-tools/mgradm/shared/ssl/testdata07070100000085000081A4000003E80000006400000001660688CE0000073E000000000000000000000000000000000000003200000000uyuni-tools/mgradm/shared/ssl/testdata/RootCA.key-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIZDERQAIgS+ACAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBA0CZ/wEFCBHg0mYxZucHcwBIIE
wI40HqL0NoUUuxx0qo+UVoXuTPp5bWEmv4be+0v3+ya0ucCLx41l7HiYSwL/fYvt
tStWm9ArOlLW48uxF54eyA3Nwvt59xnZBGN0YokJR1heeYzns1xadgHwiFjl4CLI
IfpUoQvGmnph51301YRgwuTRGJsih8EH8g24TQ5qy1uRMS1BZLd7elUuHzVFyu6y
PMfxpqcygBvjnufgOye4aAkTQIeGlJBDk5DwVx56T5VEUA8/v9VXVFy4took8Gwr
B08ZVszbR7AeUpa16aqKXFV1hQPcEle/n/0zsqPTE0drRxAGKufdpvK1rfDRDSQx
LudQJ31NJkCBRe+sP+rv4BA+zme4xFEjAI12IVQ6sUaTCW/qMnPBcSC5iSvVLXen
iSB519BJgLPmJi2gNXsuluHddwRp1QFUOoxnyPHDyGFkQ4zNwnG/pPvxASoBQRPA
PR/JsZC9p+oGHBds0HzZETuU5oKWdrZwyawQaAWEeuZEeICsrkrKi9PkumY5SMCT
47tn0vyQTQEGexKag/UTaC5PaGX8SxpWBXrskxgLcYnUAQnpkEhnU92CJQSI36uL
pGC7kQKY8vPjCOw33lLrEkWDxIn3oFX76NuHtMBdKgNT/Qlxb1tqIfuaNI80MrGV
zAzeK6dw4ZakI6aSlOBa4l71JB3HfWZjWAldhS33GvWAdsnPef5c5jMEsDqMFFnw
lZh2hrdfUTkAb+v2tJ13CUccSqIy3i9DmOU/ijdEGGte3E1ws+qyouHsFC09ETcy
XC4YYXF4ccGNZemcqpsQhi5iQEL+HjaNRkv16+qyXRUG49TGRe8nlEA2mBXV8Wzf
mkEdzp9Oc3iOPqeQYgnhtHbYaj0iHjiwaUmKiisadD7Jo76z2CAG0YJdWw4FNNUI
tpM2eADVHXcFGLDzvmFWtPnfkBnhn09GhOJJXOIEsoNyhczv6TqMrtGx8wYvbNqc
YOgl42mDYn6v0P+uEDWcHWFQiHNDNPHUCT/LmjYcVRTOPlCqnv5Fh+yKS50pGtFy
8h53QURP7e4cJxE8CuBZAAGiEkoXEbXGoslnrtpQbB0bVpMDKrLpJMrMjSr4/pKK
zWRgVo7wOuUJN+o30lcQ/RM8NzJRii6tnHRG1eOijNOLqRYmyEHFSeTB7+lhjE5A
xyXotHOml32pW4lEu7Ks18fmTQtyI1opx4ocVLNMeRvesxCf3z5eMUciXG+frxnF
a9UScTR1rIbVVP2V7PiHVCXY6WXZqiefWU/Sn+oVndm/9GGH9f2WVW1xn6YX3L+e
pTpzZScReh7B0gd3cVtwmfAbQU3xuA8IFJnrLuDwQCs19WweImgPm/rf7ITBuxeO
3vdvVd8GfCsN3sTJ/9G5XJJstM0eXoXjoXhthQ4OuTyVSiI3tHROYW+iggAJsaVH
HwzTnGE5m5WBvQ9GziSThxDz4vfDtwVlNS1K0+wkt9stt9ruCpXxntQV2FEin0GM
iWptJuYpCMOH9gkALDYoYv/jG0PbMBs63FRLeZg2ehtYw6kU5KtjHkLZTiccuwSq
W010SdsstOrwrZKEJAuRNQHLHRFmjyoskz0nCtmyJBCTkP46Awu5PFVwBE9auiIe
Y0UUre0B4tPsoPewpvgRJio=
-----END ENCRYPTED PRIVATE KEY-----
07070100000086000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002E00000000uyuni-tools/mgradm/shared/ssl/testdata/chain107070100000087000081A4000003E80000006400000001660688CE000027CA000000000000000000000000000000000000004200000000uyuni-tools/mgradm/shared/ssl/testdata/chain1/intermediate-ca.crtCertificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            67:63:f5:29:af:44:ba:79:ef:dd:dc:bf:bb:ab:3e:36:22:b8:71:26
        Signature Algorithm: sha384WithRSAEncryption
        Issuer: C=DE, ST=STATE, L=CITY, O=ORG, OU=ORGUNIT, CN=RootCA
        Validity
            Not Before: Oct  2 13:09:11 2023 GMT
            Not After : Feb 13 13:09:11 2025 GMT
        Subject: C=DE, ST=STATE, O=ORG, OU=ORGUNIT, CN=OrgCa
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:e0:25:1f:af:4d:59:23:82:a6:f1:c9:33:d1:c6:
                    10:47:ad:f4:f2:b6:96:aa:79:2b:45:97:d3:d8:a5:
                    29:fe:9c:b9:2c:26:30:37:5f:ae:69:6a:ac:85:e0:
                    28:d3:d3:7e:83:c8:87:2a:70:e5:a6:78:99:d9:42:
                    f9:d3:17:c0:e1:bc:4c:51:af:f0:aa:a8:fb:19:65:
                    27:91:35:80:5a:3d:fd:90:0b:fc:af:a1:d3:af:26:
                    02:ba:7c:26:bc:aa:08:6e:cf:d5:5d:ed:c9:8c:33:
                    9f:63:51:45:49:1f:f2:1f:12:c2:7c:e4:42:05:8a:
                    ef:33:d3:0e:e5:44:58:99:88:aa:2f:e3:5f:37:d8:
                    36:fb:21:ac:90:0f:82:b9:55:bc:9e:ba:23:70:4a:
                    83:c0:44:37:c2:0a:9a:03:fb:1d:4a:d2:67:a8:70:
                    e0:8c:b2:c1:d7:d8:e7:c9:bd:ee:6f:f6:4e:f7:25:
                    f2:4b:9b:93:33:28:40:18:c6:f1:47:78:0d:84:fa:
                    7f:f4:82:9b:37:f0:37:84:25:b5:ae:f5:88:4f:d2:
                    d9:7e:61:c0:8e:92:24:c8:32:55:cb:c4:8c:e6:be:
                    1f:e8:32:e2:9f:18:1e:2f:a6:8f:80:27:d4:77:7b:
                    5d:2d:cb:eb:a4:b8:2f:28:a0:38:34:a5:91:c8:6e:
                    b6:71
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:TRUE
            X509v3 Key Usage: 
                Digital Signature, Key Encipherment, Certificate Sign
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            Netscape Comment: 
                SSL Generated Certificate
            X509v3 Subject Key Identifier: 
                9B:C1:07:5E:AB:5C:7E:6B:9E:E4:23:7B:18:61:34:CB:D0:06:91:3B
            X509v3 Authority Key Identifier: 
                keyid:6E:6D:4B:35:22:23:3E:13:18:A5:93:61:0E:9C:BE:1E:D2:B8:1B:D4
                DirName:/C=DE/ST=STATE/L=CITY/O=ORG/OU=ORGUNIT/CN=RootCA
                serial:49:96:04:48:85:CB:0D:EC:2C:31:FE:EF:E9:CB:12:2B:DB:80:F8:71
    Signature Algorithm: sha384WithRSAEncryption
    Signature Value:
        78:5a:ac:de:87:a3:fb:d5:e1:29:4f:c3:1b:39:2b:da:29:78:
        1c:07:3d:e3:db:da:8a:40:3d:c3:d4:51:9d:21:59:d2:37:66:
        f5:47:69:b8:96:2a:2e:f0:35:1a:5b:5b:23:cd:d5:ac:88:49:
        97:e1:5b:91:e4:b8:7a:2d:ab:46:17:c4:61:a9:1a:b1:29:d3:
        50:52:af:0d:c2:4a:e1:2f:aa:00:1b:07:5a:7d:f9:d1:57:19:
        66:49:52:c5:74:3c:1e:3d:a3:1f:49:64:60:92:48:03:a2:37:
        52:26:69:24:34:d7:a4:68:fd:ea:b7:a6:d6:c2:b0:46:19:a7:
        2c:b9:cc:a3:0f:87:4c:cb:fb:69:d1:a9:c5:93:73:69:7c:34:
        aa:3d:f0:98:83:88:14:48:29:2d:ed:f9:c0:96:22:ae:03:7a:
        2f:09:ad:43:6d:a3:12:d5:8c:48:e6:65:ca:e1:97:b4:ec:d7:
        aa:fc:db:e2:cf:16:30:2c:46:f3:dd:a5:37:db:d9:0f:99:c5:
        74:e7:21:2d:ca:2b:c5:4b:50:56:0a:2c:0d:25:30:56:39:87:
        33:b2:ae:d7:98:74:e9:3d:ce:78:ca:1b:bd:ea:f8:a6:3f:2a:
        a4:21:3b:19:9e:b5:0d:e6:6e:d8:d2:d6:b0:e2:aa:ce:54:e6:
        51:98:65:ee
-----BEGIN CERTIFICATE-----
MIIEXjCCA0agAwIBAgIUZ2P1Ka9Eunnv3dy/u6s+NiK4cSYwDQYJKoZIhvcNAQEM
BQAwXTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQ0wCwYDVQQHDARDSVRZ
MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlJvb3RD
QTAeFw0yMzEwMDIxMzA5MTFaFw0yNTAyMTMxMzA5MTFaME0xCzAJBgNVBAYTAkRF
MQ4wDAYDVQQIDAVTVEFURTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklU
MQ4wDAYDVQQDDAVPcmdDYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AOAlH69NWSOCpvHJM9HGEEet9PK2lqp5K0WX09ilKf6cuSwmMDdfrmlqrIXgKNPT
foPIhypw5aZ4mdlC+dMXwOG8TFGv8Kqo+xllJ5E1gFo9/ZAL/K+h068mArp8Jryq
CG7P1V3tyYwzn2NRRUkf8h8SwnzkQgWK7zPTDuVEWJmIqi/jXzfYNvshrJAPgrlV
vJ66I3BKg8BEN8IKmgP7HUrSZ6hw4IyywdfY58m97m/2Tvcl8kubkzMoQBjG8Ud4
DYT6f/SCmzfwN4Qlta71iE/S2X5hwI6SJMgyVcvEjOa+H+gy4p8YHi+mj4An1Hd7
XS3L66S4LyigODSlkchutnECAwEAAaOCASQwggEgMAwGA1UdEwQFMAMBAf8wCwYD
VR0PBAQDAgKkMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAoBglghkgB
hvhCAQ0EGxYZU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUm8EH
Xqtcfmue5CN7GGE0y9AGkTswgZoGA1UdIwSBkjCBj4AUbm1LNSIjPhMYpZNhDpy+
HtK4G9ShYaRfMF0xCzAJBgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFURTENMAsGA1UE
BwwEQ0lUWTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQD
DAZSb290Q0GCFEmWBEiFyw3sLDH+7+nLEivbgPhxMA0GCSqGSIb3DQEBDAUAA4IB
AQB4Wqzeh6P71eEpT8MbOSvaKXgcBz3j29qKQD3D1FGdIVnSN2b1R2m4liou8DUa
W1sjzdWsiEmX4VuR5Lh6LatGF8RhqRqxKdNQUq8NwkrhL6oAGwdaffnRVxlmSVLF
dDwePaMfSWRgkkgDojdSJmkkNNekaP3qt6bWwrBGGacsucyjD4dMy/tp0anFk3Np
fDSqPfCYg4gUSCkt7fnAliKuA3ovCa1DbaMS1YxI5mXK4Ze07Neq/NvizxYwLEbz
3aU329kPmcV05yEtyivFS1BWCiwNJTBWOYczsq7XmHTpPc54yhu96vimPyqkITsZ
nrUN5m7Y0taw4qrOVOZRmGXu
-----END CERTIFICATE-----
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            67:63:f5:29:af:44:ba:79:ef:dd:dc:bf:bb:ab:3e:36:22:b8:71:27
        Signature Algorithm: sha384WithRSAEncryption
        Issuer: C=DE, ST=STATE, O=ORG, OU=ORGUNIT, CN=OrgCa
        Validity
            Not Before: Oct  2 13:09:11 2023 GMT
            Not After : Nov  5 13:09:11 2024 GMT
        Subject: C=DE, ST=STATE, O=ORG, OU=ORGUNIT, CN=TeamCA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:eb:35:fe:18:8f:59:de:23:4a:84:bc:de:7b:f1:
                    79:f8:1a:5d:94:95:54:2c:00:bd:42:c1:e6:f5:c6:
                    ca:25:da:97:cd:5b:85:d6:89:8a:7c:45:11:9e:df:
                    65:10:68:e4:49:6a:cf:fd:76:48:08:c7:09:aa:e3:
                    88:c2:7e:2f:f9:85:b4:df:d4:00:ec:a9:71:38:1d:
                    ff:8d:d4:d1:84:2a:f9:9b:e7:7f:e1:61:3e:75:06:
                    7f:18:66:59:23:96:e6:c2:75:11:e7:f4:f3:47:7b:
                    e6:17:8c:25:d9:ee:da:01:d6:cd:94:9e:a7:8e:35:
                    f6:d8:24:d6:cf:58:4f:29:36:42:18:96:aa:87:ca:
                    ad:af:05:a2:e5:a6:6b:4f:42:98:e3:4e:86:b4:d7:
                    1f:2f:db:c3:5b:bd:e9:da:7d:d0:d9:8d:83:c9:28:
                    56:27:e7:0d:a2:15:88:99:af:eb:a3:85:73:9e:3d:
                    64:70:01:be:cb:71:c0:d8:ca:e7:6e:25:b7:3b:fe:
                    73:0a:92:d2:23:2d:f5:f4:9c:0e:d6:65:c6:ef:6c:
                    9a:c5:c5:af:70:10:ba:fc:2d:b1:29:26:88:9e:06:
                    e5:63:5f:d4:25:0c:98:18:f0:46:77:86:f5:98:00:
                    63:38:3a:36:81:27:94:2a:cc:84:24:75:01:54:ed:
                    a4:d7
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:TRUE
            X509v3 Key Usage: 
                Digital Signature, Key Encipherment, Certificate Sign
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            Netscape Comment: 
                SSL Generated Certificate
            X509v3 Subject Key Identifier: 
                62:00:25:E4:EE:70:E5:37:2D:1E:9E:AE:4E:B7:3E:FC:62:08:BF:27
            X509v3 Authority Key Identifier: 
                keyid:9B:C1:07:5E:AB:5C:7E:6B:9E:E4:23:7B:18:61:34:CB:D0:06:91:3B
                DirName:/C=DE/ST=STATE/L=CITY/O=ORG/OU=ORGUNIT/CN=RootCA
                serial:67:63:F5:29:AF:44:BA:79:EF:DD:DC:BF:BB:AB:3E:36:22:B8:71:26
    Signature Algorithm: sha384WithRSAEncryption
    Signature Value:
        26:a0:7e:98:44:58:ab:81:9f:f9:a6:04:dc:08:59:d8:b4:5a:
        11:47:8e:9c:23:7f:53:66:f9:b9:93:5b:df:50:d6:2a:11:a1:
        cb:1c:d5:2e:cd:5d:f3:eb:45:b5:fe:01:9f:0c:d0:f0:1d:8f:
        57:ac:0f:2a:5b:a4:6a:57:a7:0e:25:e1:69:25:f5:ef:2f:3c:
        60:9c:26:ac:e8:cd:3a:89:fa:84:18:da:bb:83:f6:f5:02:53:
        51:f2:ab:76:e8:fb:d0:63:dc:5c:09:c5:f7:de:68:90:c0:50:
        80:ec:88:ff:16:95:a0:c1:97:69:fb:1f:9d:43:32:0c:5d:f9:
        bc:5e:48:c4:52:f2:f3:43:1f:ff:c5:bb:58:6c:ee:11:cb:0c:
        22:45:29:1c:62:26:78:9c:31:10:d8:14:24:17:17:4a:7d:a9:
        1d:3b:5b:64:8e:b2:84:91:66:fb:f2:e6:37:3d:c2:1b:db:98:
        11:10:6d:67:9c:95:a5:d9:a4:8a:e1:b6:c4:ab:2d:f7:48:3a:
        66:9b:9c:af:9b:b7:a5:27:cc:4b:53:e6:21:0c:2b:6c:c8:b2:
        cc:6c:51:58:df:b2:bc:53:ed:25:0f:4a:e6:44:6c:be:74:46:
        0b:6a:df:46:76:cd:c3:8d:c0:9b:93:3c:93:50:22:a1:f4:fa:
        38:9a:85:1f
-----BEGIN CERTIFICATE-----
MIIETzCCAzegAwIBAgIUZ2P1Ka9Eunnv3dy/u6s+NiK4cScwDQYJKoZIhvcNAQEM
BQAwTTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQwwCgYDVQQKDANPUkcx
EDAOBgNVBAsMB09SR1VOSVQxDjAMBgNVBAMMBU9yZ0NhMB4XDTIzMTAwMjEzMDkx
MVoXDTI0MTEwNTEzMDkxMVowTjELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRF
MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlRlYW1D
QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOs1/hiPWd4jSoS83nvx
efgaXZSVVCwAvULB5vXGyiXal81bhdaJinxFEZ7fZRBo5Elqz/12SAjHCarjiMJ+
L/mFtN/UAOypcTgd/43U0YQq+Zvnf+FhPnUGfxhmWSOW5sJ1Eef080d75heMJdnu
2gHWzZSep4419tgk1s9YTyk2QhiWqofKra8FouWma09CmONOhrTXHy/bw1u96dp9
0NmNg8koVifnDaIViJmv66OFc549ZHABvstxwNjK524ltzv+cwqS0iMt9fScDtZl
xu9smsXFr3AQuvwtsSkmiJ4G5WNf1CUMmBjwRneG9ZgAYzg6NoEnlCrMhCR1AVTt
pNcCAwEAAaOCASQwggEgMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgKkMB0GA1Ud
JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAoBglghkgBhvhCAQ0EGxYZU1NMIEdl
bmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUYgAl5O5w5TctHp6uTrc+/GII
vycwgZoGA1UdIwSBkjCBj4AUm8EHXqtcfmue5CN7GGE0y9AGkTuhYaRfMF0xCzAJ
BgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFURTENMAsGA1UEBwwEQ0lUWTEMMAoGA1UE
CgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQDDAZSb290Q0GCFGdj9Smv
RLp5793cv7urPjYiuHEmMA0GCSqGSIb3DQEBDAUAA4IBAQAmoH6YRFirgZ/5pgTc
CFnYtFoRR46cI39TZvm5k1vfUNYqEaHLHNUuzV3z60W1/gGfDNDwHY9XrA8qW6Rq
V6cOJeFpJfXvLzxgnCas6M06ifqEGNq7g/b1AlNR8qt26PvQY9xcCcX33miQwFCA
7Ij/FpWgwZdp+x+dQzIMXfm8XkjEUvLzQx//xbtYbO4RywwiRSkcYiZ4nDEQ2BQk
FxdKfakdO1tkjrKEkWb78uY3PcIb25gREG1nnJWl2aSK4bbEqy33SDpmm5yvm7el
J8xLU+YhDCtsyLLMbFFY37K8U+0lD0rmRGy+dEYLat9Gds3DjcCbkzyTUCKh9Po4
moUf
-----END CERTIFICATE-----
07070100000088000081A4000003E80000006400000001660688CE000013BA000000000000000000000000000000000000003A00000000uyuni-tools/mgradm/shared/ssl/testdata/chain1/root-ca.crtCertificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            49:96:04:48:85:cb:0d:ec:2c:31:fe:ef:e9:cb:12:2b:db:80:f8:71
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = DE, ST = STATE, L = CITY, O = ORG, OU = ORGUNIT, CN = RootCA
        Validity
            Not Before: Oct  2 13:09:10 2023 GMT
            Not After : Jul 22 13:09:10 2026 GMT
        Subject: C = DE, ST = STATE, L = CITY, O = ORG, OU = ORGUNIT, CN = RootCA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:f3:67:90:0c:b8:98:e1:5c:8d:00:32:13:26:95:
                    95:6f:7c:2f:96:34:b8:6e:2f:5a:61:da:e2:bd:2e:
                    9e:8a:ad:5e:4e:27:a9:1c:08:06:7c:36:26:28:5e:
                    a7:6e:bc:04:68:eb:2c:97:b6:4b:ca:f0:0d:9c:5a:
                    47:ee:9e:15:1e:c0:62:3c:72:1b:80:01:07:67:51:
                    64:34:0f:41:50:73:21:09:d9:79:ac:73:51:db:5c:
                    a0:30:fa:79:49:02:a4:8e:cb:8f:15:dd:99:4c:b7:
                    9e:d1:ec:18:f9:6f:d2:73:27:d1:ff:c9:07:07:4e:
                    8a:6e:02:2d:6d:ab:5b:5f:5b:a2:4a:4d:c7:d6:7b:
                    26:6e:b7:a0:44:0d:82:18:43:f8:3a:49:f7:47:40:
                    d0:ed:72:dd:f2:8b:c4:9f:e5:64:24:49:f0:0d:e8:
                    5b:21:66:89:31:4a:3e:1e:9c:9b:11:89:91:9d:57:
                    af:73:64:19:bf:ed:02:8f:3f:0b:5f:aa:2c:5c:93:
                    9b:03:08:c4:a1:72:58:7a:df:cb:f2:00:8c:71:7e:
                    76:23:29:ac:c6:6a:46:2a:a9:b2:6e:f4:14:2a:16:
                    e8:7b:3c:f4:c3:14:89:11:54:d3:10:70:6c:98:c8:
                    66:e3:f7:31:cf:fd:78:76:e2:eb:2a:3e:37:a6:ce:
                    4c:07
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:TRUE
            X509v3 Key Usage: 
                Digital Signature, Key Encipherment, Certificate Sign
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            Netscape Comment: 
                SSL Generated Certificate
            X509v3 Subject Key Identifier: 
                6E:6D:4B:35:22:23:3E:13:18:A5:93:61:0E:9C:BE:1E:D2:B8:1B:D4
            X509v3 Authority Key Identifier: 
                DirName:/C=DE/ST=STATE/L=CITY/O=ORG/OU=ORGUNIT/CN=RootCA
                serial:49:96:04:48:85:CB:0D:EC:2C:31:FE:EF:E9:CB:12:2B:DB:80:F8:71
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        82:e6:ae:cb:60:cf:85:3e:00:70:37:c7:dc:c9:51:b9:70:36:
        25:e2:f5:bc:e0:8f:34:3d:67:1f:09:8e:48:e9:de:b5:78:b5:
        b5:97:f6:75:fa:fc:0f:05:c4:e1:33:ab:f5:f9:b1:32:9f:75:
        b3:c4:fd:a9:6d:c6:88:c6:a5:35:68:28:04:1d:c0:1d:92:9a:
        9b:be:52:e9:b9:9c:0d:01:b1:a8:0d:42:89:f7:f3:43:58:99:
        98:6c:0d:9f:ff:9d:10:29:68:9f:db:41:e7:b7:c6:43:67:79:
        ec:a6:f2:5b:ce:b7:d9:17:90:c2:f4:ac:56:8f:9a:af:fb:85:
        85:59:95:d1:5e:37:f6:40:2a:16:cf:53:fa:55:8b:35:48:31:
        10:c6:c4:b9:85:07:96:48:c3:dd:35:d0:04:e3:c8:fd:7e:8e:
        a2:ab:60:6b:b4:cc:f5:33:44:f8:bc:e6:b1:1b:86:f0:6d:a1:
        23:48:62:63:de:e3:27:d7:8c:9b:58:a2:10:ed:11:b6:b8:4c:
        ee:83:4a:be:0b:ee:6a:80:1d:02:91:21:4d:84:64:5f:a5:1e:
        1b:c5:8c:c6:d9:7d:0c:43:da:45:c9:13:e5:47:46:8d:bb:36:
        51:f8:72:70:d7:40:43:97:3b:71:fc:55:90:00:6e:8f:d3:c9:
        c6:73:67:15
-----BEGIN CERTIFICATE-----
MIIEVjCCAz6gAwIBAgIUSZYESIXLDewsMf7v6csSK9uA+HEwDQYJKoZIhvcNAQEL
BQAwXTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQ0wCwYDVQQHDARDSVRZ
MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlJvb3RD
QTAeFw0yMzEwMDIxMzA5MTBaFw0yNjA3MjIxMzA5MTBaMF0xCzAJBgNVBAYTAkRF
MQ4wDAYDVQQIDAVTVEFURTENMAsGA1UEBwwEQ0lUWTEMMAoGA1UECgwDT1JHMRAw
DgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQDDAZSb290Q0EwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDzZ5AMuJjhXI0AMhMmlZVvfC+WNLhuL1ph2uK9Lp6K
rV5OJ6kcCAZ8NiYoXqduvARo6yyXtkvK8A2cWkfunhUewGI8chuAAQdnUWQ0D0FQ
cyEJ2Xmsc1HbXKAw+nlJAqSOy48V3ZlMt57R7Bj5b9JzJ9H/yQcHTopuAi1tq1tf
W6JKTcfWeyZut6BEDYIYQ/g6SfdHQNDtct3yi8Sf5WQkSfAN6FshZokxSj4enJsR
iZGdV69zZBm/7QKPPwtfqixck5sDCMShclh638vyAIxxfnYjKazGakYqqbJu9BQq
Fuh7PPTDFIkRVNMQcGyYyGbj9zHP/Xh24usqPjemzkwHAgMBAAGjggEMMIIBCDAM
BgNVHRMEBTADAQH/MAsGA1UdDwQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwKAYJYIZIAYb4QgENBBsWGVNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNh
dGUwHQYDVR0OBBYEFG5tSzUiIz4TGKWTYQ6cvh7SuBvUMIGCBgNVHSMEezB5oWGk
XzBdMQswCQYDVQQGEwJERTEOMAwGA1UECAwFU1RBVEUxDTALBgNVBAcMBENJVFkx
DDAKBgNVBAoMA09SRzEQMA4GA1UECwwHT1JHVU5JVDEPMA0GA1UEAwwGUm9vdENB
ghRJlgRIhcsN7Cwx/u/pyxIr24D4cTANBgkqhkiG9w0BAQsFAAOCAQEAguauy2DP
hT4AcDfH3MlRuXA2JeL1vOCPND1nHwmOSOnetXi1tZf2dfr8DwXE4TOr9fmxMp91
s8T9qW3GiMalNWgoBB3AHZKam75S6bmcDQGxqA1CiffzQ1iZmGwNn/+dEClon9tB
57fGQ2d57KbyW8632ReQwvSsVo+ar/uFhVmV0V439kAqFs9T+lWLNUgxEMbEuYUH
lkjD3TXQBOPI/X6Ooqtga7TM9TNE+LzmsRuG8G2hI0hiY97jJ9eMm1iiEO0RtrhM
7oNKvgvuaoAdApEhTYRkX6UeG8WMxtl9DEPaRckT5UdGjbs2UfhycNdAQ5c7cfxV
kABuj9PJxnNnFQ==
-----END CERTIFICATE-----
07070100000089000081A4000003E80000006400000001660688CE0000148A000000000000000000000000000000000000003900000000uyuni-tools/mgradm/shared/ssl/testdata/chain1/server.crtCertificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            67:63:f5:29:af:44:ba:79:ef:dd:dc:bf:bb:ab:3e:36:22:b8:71:28
        Signature Algorithm: sha384WithRSAEncryption
        Issuer: C=DE, ST=STATE, O=ORG, OU=ORGUNIT, CN=TeamCA
        Validity
            Not Before: Oct  2 13:09:12 2023 GMT
            Not After : Oct  1 13:09:12 2024 GMT
        Subject: C=DE, ST=STATE, O=ORG, OU=ORGUNIT, CN=uyuni-server-cert
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:83:51:78:0e:31:92:ea:d9:51:6e:9d:02:ed:d8:
                    55:dc:53:5f:7e:0c:a5:26:df:e1:c1:86:e0:38:9d:
                    67:59:e2:42:21:37:4d:5d:2c:f7:28:f1:d4:7f:00:
                    91:e4:0e:fa:eb:c8:bb:2d:f2:cd:37:62:5e:94:67:
                    83:0a:e0:69:4d:86:f4:39:be:1b:59:ec:64:65:41:
                    0c:5a:3a:7e:4b:98:8e:62:4c:4b:2c:b7:68:3c:36:
                    e2:ea:7e:58:70:e7:3e:7e:0a:b4:7f:32:b7:d0:0f:
                    15:b4:ac:24:ce:a8:f4:13:9e:62:44:7d:f6:3e:fd:
                    a5:67:5a:3d:67:54:40:89:6f:51:f0:4a:60:35:d4:
                    51:27:ca:bb:a1:5e:32:12:a5:3f:b4:e3:d0:7d:9e:
                    b2:84:4f:4e:84:db:52:00:9e:46:bf:c8:31:3b:79:
                    30:09:fe:ba:01:b7:3e:c4:9b:c4:cc:18:8b:f0:42:
                    fd:88:71:8c:d6:4c:d2:99:c8:7a:bf:7e:d6:dc:18:
                    d2:97:57:ca:0c:0c:66:52:2e:38:1c:8a:56:13:44:
                    a5:84:3d:95:70:e5:aa:94:2f:59:48:3d:22:74:8d:
                    f8:5d:ef:42:9c:e1:cf:ca:f5:24:d8:a8:8c:4e:1d:
                    75:9d:ac:15:a9:6f:f7:81:ab:5e:11:61:f7:8f:e3:
                    a5:c1
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:FALSE
            X509v3 Key Usage: 
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            Netscape Cert Type: 
                SSL Server
            Netscape Comment: 
                SSL Generated Certificate
            X509v3 Subject Key Identifier: 
                FD:F8:AA:26:08:7F:F7:FA:92:1E:68:3F:4A:29:54:E5:7A:A6:0D:42
            X509v3 Authority Key Identifier: 
                keyid:62:00:25:E4:EE:70:E5:37:2D:1E:9E:AE:4E:B7:3E:FC:62:08:BF:27
                DirName:/C=DE/ST=STATE/O=ORG/OU=ORGUNIT/CN=OrgCa
                serial:67:63:F5:29:AF:44:BA:79:EF:DD:DC:BF:BB:AB:3E:36:22:B8:71:27
            X509v3 Subject Alternative Name: 
                DNS:*.example.com
    Signature Algorithm: sha384WithRSAEncryption
    Signature Value:
        24:4e:b4:4d:29:e6:ad:12:e6:39:9d:95:0e:fc:b7:af:e3:55:
        60:cc:f2:57:1c:38:05:fb:1f:8c:95:40:40:b3:23:c3:11:1b:
        5f:7b:99:01:0d:fe:3a:05:7e:d0:b1:9a:c8:fc:6a:78:41:fb:
        3f:5a:6a:26:0d:dd:2e:f3:ab:d5:16:45:99:51:3e:94:87:3a:
        a7:67:e4:25:43:c9:1a:e5:84:df:15:ba:f3:11:64:99:1d:22:
        0e:44:35:9c:9b:52:e8:b0:a8:a9:04:d2:4b:cf:10:14:35:6c:
        1f:a3:ec:81:4f:2b:98:c0:02:8a:b9:03:50:f3:97:25:25:05:
        ba:c4:e0:5b:b2:34:7e:d3:d6:e1:69:d8:72:38:9e:0a:50:f6:
        25:d0:97:c3:58:37:49:40:08:11:9d:39:b1:4b:4d:e9:18:08:
        38:9d:b5:b4:a2:8b:d8:24:94:b3:b5:41:e9:17:b8:22:17:6e:
        70:33:a2:a3:ee:8e:ae:be:ee:c4:dd:6c:2c:c2:ae:8b:31:8f:
        5d:ca:9d:01:83:5d:89:59:cf:f6:30:3a:59:4d:17:82:ab:6e:
        a3:bf:4d:61:98:9a:f6:29:20:9b:eb:c5:3a:cd:06:b6:82:8c:
        24:34:65:60:12:6d:4b:ca:e6:91:0a:58:c8:97:22:89:f5:f4:
        3f:16:33:4e
-----BEGIN CERTIFICATE-----
MIIEdDCCA1ygAwIBAgIUZ2P1Ka9Eunnv3dy/u6s+NiK4cSgwDQYJKoZIhvcNAQEM
BQAwTjELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQwwCgYDVQQKDANPUkcx
EDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlRlYW1DQTAeFw0yMzEwMDIxMzA5
MTJaFw0yNDEwMDExMzA5MTJaMFkxCzAJBgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFU
RTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMRowGAYDVQQDDBF1eXVu
aS1zZXJ2ZXItY2VydDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAINR
eA4xkurZUW6dAu3YVdxTX34MpSbf4cGG4DidZ1niQiE3TV0s9yjx1H8AkeQO+uvI
uy3yzTdiXpRngwrgaU2G9Dm+G1nsZGVBDFo6fkuYjmJMSyy3aDw24up+WHDnPn4K
tH8yt9APFbSsJM6o9BOeYkR99j79pWdaPWdUQIlvUfBKYDXUUSfKu6FeMhKlP7Tj
0H2esoRPToTbUgCeRr/IMTt5MAn+ugG3PsSbxMwYi/BC/YhxjNZM0pnIer9+1twY
0pdXygwMZlIuOByKVhNEpYQ9lXDlqpQvWUg9InSN+F3vQpzhz8r1JNiojE4ddZ2s
Falv94GrXhFh94/jpcECAwEAAaOCAT0wggE5MAkGA1UdEwQCMAAwCwYDVR0PBAQD
AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjARBglghkgBhvhCAQEE
BAMCBkAwKAYJYIZIAYb4QgENBBsWGVNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUw
HQYDVR0OBBYEFP34qiYIf/f6kh5oP0opVOV6pg1CMIGJBgNVHSMEgYEwf4AUYgAl
5O5w5TctHp6uTrc+/GIIvyehUaRPME0xCzAJBgNVBAYTAkRFMQ4wDAYDVQQIDAVT
VEFURTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQ4wDAYDVQQDDAVP
cmdDYYIUZ2P1Ka9Eunnv3dy/u6s+NiK4cScwGAYDVR0RBBEwD4INKi5leGFtcGxl
LmNvbTANBgkqhkiG9w0BAQwFAAOCAQEAJE60TSnmrRLmOZ2VDvy3r+NVYMzyVxw4
BfsfjJVAQLMjwxEbX3uZAQ3+OgV+0LGayPxqeEH7P1pqJg3dLvOr1RZFmVE+lIc6
p2fkJUPJGuWE3xW68xFkmR0iDkQ1nJtS6LCoqQTSS88QFDVsH6PsgU8rmMACirkD
UPOXJSUFusTgW7I0ftPW4WnYcjieClD2JdCXw1g3SUAIEZ05sUtN6RgIOJ21tKKL
2CSUs7VB6Re4IhducDOio+6Orr7uxN1sLMKuizGPXcqdAYNdiVnP9jA6WU0Xgqtu
o79NYZia9ikgm+vFOs0GtoKMJDRlYBJtS8rmkQpYyJciifX0PxYzTg==
-----END CERTIFICATE-----
0707010000008A000081A4000003E80000006400000001660688CE000006A8000000000000000000000000000000000000003900000000uyuni-tools/mgradm/shared/ssl/testdata/chain1/server.key-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCDUXgOMZLq2VFu
nQLt2FXcU19+DKUm3+HBhuA4nWdZ4kIhN01dLPco8dR/AJHkDvrryLst8s03Yl6U
Z4MK4GlNhvQ5vhtZ7GRlQQxaOn5LmI5iTEsst2g8NuLqflhw5z5+CrR/MrfQDxW0
rCTOqPQTnmJEffY+/aVnWj1nVECJb1HwSmA11FEnyruhXjISpT+049B9nrKET06E
21IAnka/yDE7eTAJ/roBtz7Em8TMGIvwQv2IcYzWTNKZyHq/ftbcGNKXV8oMDGZS
LjgcilYTRKWEPZVw5aqUL1lIPSJ0jfhd70Kc4c/K9STYqIxOHXWdrBWpb/eBq14R
YfeP46XBAgMBAAECggEAHjz9Sy9pKEEAelsXWJNvOfvMyma5BNmaz4hySzcbnFv4
ZFOqseDvzPLavp/v+Dbm2rJvP6ZgUPeK1dt8Fl4UgXCo/j7jZ3KCr7op0QEVIe0w
JDxzNwnIq8zrtZmAXgcxoa5vX7bbEsLWebMGCrxm77mR4TmsIVcg5kqmRwvkjIDM
SZxAJSMYVWlmyI695fMPng8f4nOxRWPBgW73XzMvlyr68OUtji9+JqI8C9mJ471S
F2qL+ubaovZM83EQ1gAol5RX8rDdkQD+/OlkLaVvdKOpp3VLKx1KszYyqhR/wYqD
4FUjK5Abxz8JlVOZkPOaF3Y9IgAuAhK45lqyUjGtvQKBgQC3wTzcpB2zjmw07iAP
B0/REU1RGPvkehsV3KnjsSvWAzrgAJMgS1l5pWJLLBRJDguX3jbDMiQ05Z9RSLdL
+IbvL75+QhbXLRiv7aGkM1xIaBrGJpCANZmHKjjy/T7LfewSwyNqlP/FdMOy+tat
2/t/IrmpArryd/cP/Pw8rfKS0wKBgQC28oixIJP3uAtQzpiqKnIovw57s8MXcMzh
QaxR00v8ss4d82T6BmJolF+gjotsYq4lVnOmL1+E/74dN8p0xL8ITIrr8AAO2Kp2
j0qWVgagvjqTq+5H7er9dRwCpKTo2dt2/JM6MCrKyNICdZdljYJiUVijTenckzeS
K78RD3BAmwKBgQCu0PNjAeuT4IJHVOhBA/bGcsx4w+kYs6ZDBTzHds26fDYt174g
8i58kX/S/muKGQekgu7cgz546J/KSADCEP3mXii/m4Z5Tdj3vn6SZZ588DXQn+3H
W7blJaEqYw2zsOe/7dAq3Pf8VZq9EvDcVLWOfW3eQc+zT7hHiKo73E0zqwKBgHa+
0a52gNRnJyEaF8lLp7F+4T21nkmWs8T5xYmO5mFtBZA3LTGD91f+BlvGagS9wF8H
0CTr1soS3SlFzykfkwcl933Q15jLVUmDFFykFcU78/VpwU36xW4iFz4387oXvfVr
V3yLSxs4Yeeqv8vwn9KFDk1hAwximc1Mi8XdCXVFAoGAVIYfKAO9KgshtbxRE3ql
kC7DhT2iZ7du8F2qLZf5tEV4WcWtxy5vI89MYHzg1MhToKPGauvOxDSqdLLuzeKa
0MWuGiV02z4nA5xv40OVWI5zylcwPeV7drCoitjvbFCpv4bKcagdVOOF8SjB25GA
yBPHa/QCsfYNCPpWHS7DYvk=
-----END PRIVATE KEY-----
0707010000008B000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002E00000000uyuni-tools/mgradm/shared/ssl/testdata/chain20707010000008C000081A4000003E80000006400000001660688CE0000061E000000000000000000000000000000000000004700000000uyuni-tools/mgradm/shared/ssl/testdata/chain2/RHN-ORG-TRUSTED-SSL-CERT-----BEGIN CERTIFICATE-----
MIIEVjCCAz6gAwIBAgIUA12e94NKtyrGIZpdEYgrqkjHXN8wDQYJKoZIhvcNAQEL
BQAwXTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQ0wCwYDVQQHDARDSVRZ
MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlJvb3RD
QTAeFw0yMzEwMDYxNTQyMzBaFw0yNjA3MjYxNTQyMzBaMF0xCzAJBgNVBAYTAkRF
MQ4wDAYDVQQIDAVTVEFURTENMAsGA1UEBwwEQ0lUWTEMMAoGA1UECgwDT1JHMRAw
DgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQDDAZSb290Q0EwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDDbR3+UOxtw6KO8s/XsvkjukqSAFggAjSJxOw+KJL4
tOykM4lBXkC3nLiV6ve5Np2koi9bX1At/nk1Fxftwy37WbeVAFs6wprkI0sDbK6z
ZfT/qRoNChpYnzMFs28VCgftsOv1q5aLEUHfnSgEIK3lH3lMvDaEO6VgTDa84Y3h
DlNbj5bssq3mMHsKE5DRCSM0wXP8ZlnwfY8S/LMxf8FN8S+c3fwg6/+dUKiAHU8Q
goXQliH/NvZZPvvYTiADTY+xt6fEeZ4OdVVV31V7so3v6cIN4WwaOtGAzWKOrB4r
Oa4ZybhmEMW7rLOnSUvl+r1UyWfh/8rH+ATSYQSynI/TAgMBAAGjggEMMIIBCDAM
BgNVHRMEBTADAQH/MAsGA1UdDwQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwKAYJYIZIAYb4QgENBBsWGVNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNh
dGUwHQYDVR0OBBYEFDOCM0j6AGn+E4QnDh2Y9rR5Mc1DMIGCBgNVHSMEezB5oWGk
XzBdMQswCQYDVQQGEwJERTEOMAwGA1UECAwFU1RBVEUxDTALBgNVBAcMBENJVFkx
DDAKBgNVBAoMA09SRzEQMA4GA1UECwwHT1JHVU5JVDEPMA0GA1UEAwwGUm9vdENB
ghQDXZ73g0q3KsYhml0RiCuqSMdc3zANBgkqhkiG9w0BAQsFAAOCAQEAbvvh+GyX
KwFC8xaGAAHBsz0yg43LS5W9TNNOospC1qwbgCpSZJ9nWBbF2UWTKUgzjSPUpCjJ
0kMUvkpFnwFujE8IgJiP0Tha3KE3D14kj91Vfs5jDSyBsexUi8GMTP4caTMbXnU5
+q1iVhigbtOh2gSBKQvTIIdhzhghp9iFX7f68WERRVlSG/xGCSt6DXO5sgUyQb2U
ArHMJZkROIrVeGY6pXp1dWB/j6iguRUTC3GJ0JfRgx5E+pgpFnjItDQ1e2/pxsQr
ikqyFuc2CAHkhlEl0oWz+yWwCQrKkZNLABWyPWtnMecIoCqZQ79EoeQ59JZzQPrg
AQKotV5y5qBInw==
-----END CERTIFICATE-----
0707010000008D000081A4000003E80000006400000001660688CE00003C9D000000000000000000000000000000000000003C00000000uyuni-tools/mgradm/shared/ssl/testdata/chain2/spacewalk.crtCertificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            11:b5:b3:c6:0e:7b:13:28:c9:e6:28:3c:f1:40:25:6d:cb:14:eb:3e
        Signature Algorithm: sha384WithRSAEncryption
        Issuer: C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = TeamCA
        Validity
            Not Before: Oct  6 15:42:31 2023 GMT
            Not After : Oct  5 15:42:31 2024 GMT
        Subject: C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = uyuni.world-co.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:b8:3f:47:da:13:fe:f4:7b:af:ff:75:fc:0b:dd:
                    e7:26:d6:34:7b:19:33:80:7d:b9:20:40:17:b8:34:
                    a4:80:3a:4c:bb:25:0c:8a:40:65:47:32:04:af:ef:
                    2d:b6:97:70:66:1e:23:28:b0:8f:98:d4:f0:2c:b6:
                    c0:40:6a:29:06:c2:8d:5e:81:5b:60:66:54:54:9f:
                    fd:77:ae:b4:62:63:87:f1:5b:fb:aa:41:cc:82:16:
                    10:3e:35:9d:99:98:63:c1:ad:2c:7b:2d:02:0e:0a:
                    af:1d:75:6d:5c:44:c1:3d:a8:28:a5:a4:53:35:10:
                    5b:58:a8:ab:54:77:ad:f4:f4:e7:5a:51:5f:75:6f:
                    05:37:fd:55:56:a2:4d:2e:3a:58:3a:a4:d6:ad:20:
                    6d:4f:7e:1d:a2:83:94:a2:6c:0c:b8:03:ba:39:55:
                    05:93:ad:7c:9f:7a:12:99:28:3e:53:9d:3a:83:bc:
                    4a:3e:6e:2e:52:e6:63:a2:fa:e7:d9:12:90:2c:5b:
                    78:52:34:92:19:19:ac:28:84:c3:25:4f:8f:f9:0d:
                    64:ef:eb:e4:bc:cd:87:89:1c:74:01:6f:e2:1a:78:
                    92:e2:2e:15:d0:8e:2b:94:69:6d:87:f4:91:f1:5d:
                    f3:47:73:95:e3:d6:80:87:93:15:6a:f7:ae:af:83:
                    b6:55
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:FALSE
            X509v3 Key Usage: 
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            Netscape Cert Type: 
                SSL Server
            Netscape Comment: 
                SSL Generated Certificate
            X509v3 Subject Key Identifier: 
                93:88:1D:52:E3:27:6E:73:35:9E:0E:AE:20:9F:E2:2E:58:41:CA:90
            X509v3 Authority Key Identifier: 
                keyid:C6:C6:FD:7F:A3:EA:C1:50:0A:7E:33:1D:50:F7:E0:94:3F:93:EA:B7
                DirName:/C=DE/ST=STATE/O=ORG/OU=ORGUNIT/CN=OrgCa
                serial:11:B5:B3:C6:0E:7B:13:28:C9:E6:28:3C:F1:40:25:6D:CB:14:EB:3D

            X509v3 Subject Alternative Name: 
                DNS:uyuni.world-co.com
    Signature Algorithm: sha384WithRSAEncryption
         75:04:ec:e4:e7:cc:3b:00:df:a3:5e:42:70:f4:30:91:17:8c:
         2a:19:58:6d:0c:0c:ff:8a:5e:b8:1f:03:e5:c5:01:7e:7f:c1:
         9d:25:3d:5a:89:df:0b:97:a4:f6:94:3b:ce:fc:11:f2:db:b2:
         4f:76:5a:4e:9a:6d:ef:b9:b5:69:db:c7:33:27:d8:8b:ce:a7:
         45:e5:12:84:38:48:b3:f3:54:6f:bf:fb:35:3f:ae:26:1a:03:
         b2:e1:55:45:97:eb:d2:b3:7e:d3:bd:f3:21:d0:34:56:51:15:
         88:e6:49:e3:ea:ac:e8:aa:5c:16:d2:95:fa:f2:d6:f6:f0:5a:
         e7:8b:c1:7e:f6:54:c5:a4:36:99:0c:ef:d9:c3:9d:d4:22:f9:
         55:d1:2b:10:ed:6c:9d:84:87:88:c2:b3:bf:ac:54:fa:3e:3d:
         42:5d:76:83:cb:9a:b9:a2:88:b3:99:31:ac:05:f2:d6:16:be:
         73:85:bd:56:49:17:6a:f6:81:e4:f7:ec:2a:38:50:11:b6:c6:
         af:6e:df:8a:97:57:f6:36:b6:ca:3c:04:e0:c6:2b:20:c2:c0:
         50:f7:21:ec:46:23:e5:3c:5d:e3:37:19:48:88:3c:40:10:fb:
         bd:86:40:52:bf:9f:81:9a:41:36:f6:e4:8c:3f:1b:f9:2d:04:
         09:00:3a:0e
-----BEGIN CERTIFICATE-----
MIIEejCCA2KgAwIBAgIUEbWzxg57EyjJ5ig88UAlbcsU6z4wDQYJKoZIhvcNAQEM
BQAwTjELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQwwCgYDVQQKDANPUkcx
EDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlRlYW1DQTAeFw0yMzEwMDYxNTQy
MzFaFw0yNDEwMDUxNTQyMzFaMFoxCzAJBgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFU
RTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMRswGQYDVQQDDBJ1eXVu
aS53b3JsZC1jby5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4
P0faE/70e6//dfwL3ecm1jR7GTOAfbkgQBe4NKSAOky7JQyKQGVHMgSv7y22l3Bm
HiMosI+Y1PAstsBAaikGwo1egVtgZlRUn/13rrRiY4fxW/uqQcyCFhA+NZ2ZmGPB
rSx7LQIOCq8ddW1cRME9qCilpFM1EFtYqKtUd6309OdaUV91bwU3/VVWok0uOlg6
pNatIG1Pfh2ig5SibAy4A7o5VQWTrXyfehKZKD5TnTqDvEo+bi5S5mOi+ufZEpAs
W3hSNJIZGawohMMlT4/5DWTv6+S8zYeJHHQBb+IaeJLiLhXQjiuUaW2H9JHxXfNH
c5Xj1oCHkxVq966vg7ZVAgMBAAGjggFCMIIBPjAJBgNVHRMEAjAAMAsGA1UdDwQE
AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEQYJYIZIAYb4QgEB
BAQDAgZAMCgGCWCGSAGG+EIBDQQbFhlTU0wgR2VuZXJhdGVkIENlcnRpZmljYXRl
MB0GA1UdDgQWBBSTiB1S4yduczWeDq4gn+IuWEHKkDCBiQYDVR0jBIGBMH+AFMbG
/X+j6sFQCn4zHVD34JQ/k+q3oVGkTzBNMQswCQYDVQQGEwJERTEOMAwGA1UECAwF
U1RBVEUxDDAKBgNVBAoMA09SRzEQMA4GA1UECwwHT1JHVU5JVDEOMAwGA1UEAwwF
T3JnQ2GCFBG1s8YOexMoyeYoPPFAJW3LFOs9MB0GA1UdEQQWMBSCEnV5dW5pLndv
cmxkLWNvLmNvbTANBgkqhkiG9w0BAQwFAAOCAQEAdQTs5OfMOwDfo15CcPQwkReM
KhlYbQwM/4peuB8D5cUBfn/BnSU9WonfC5ek9pQ7zvwR8tuyT3ZaTppt77m1advH
MyfYi86nReUShDhIs/NUb7/7NT+uJhoDsuFVRZfr0rN+073zIdA0VlEViOZJ4+qs
6KpcFtKV+vLW9vBa54vBfvZUxaQ2mQzv2cOd1CL5VdErEO1snYSHiMKzv6xU+j49
Ql12g8uauaKIs5kxrAXy1ha+c4W9VkkXavaB5PfsKjhQEbbGr27fipdX9ja2yjwE
4MYrIMLAUPch7EYj5Txd4zcZSIg8QBD7vYZAUr+fgZpBNvbkjD8b+S0ECQA6Dg==
-----END CERTIFICATE-----
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            11:b5:b3:c6:0e:7b:13:28:c9:e6:28:3c:f1:40:25:6d:cb:14:eb:3d
        Signature Algorithm: sha384WithRSAEncryption
        Issuer: C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = OrgCa
        Validity
            Not Before: Oct  6 15:42:30 2023 GMT
            Not After : Nov  9 15:42:30 2024 GMT
        Subject: C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = TeamCA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:87:88:e3:ca:8e:8a:f1:5e:1e:b4:78:1d:32:79:
                    ef:bd:51:74:fb:40:8d:85:01:98:a4:b3:73:fa:18:
                    f5:5f:7c:6c:fb:56:ad:30:ee:df:da:19:cb:db:d2:
                    f8:59:8b:15:52:6a:46:c2:1c:12:4d:ed:83:a5:67:
                    97:47:9e:98:94:78:e2:fd:e4:7e:18:48:12:92:29:
                    54:49:ac:bb:8e:de:db:c2:22:37:a9:4f:0d:ff:39:
                    5a:ca:98:2b:fd:b5:ec:e2:e1:88:2a:cf:b6:3a:60:
                    1b:11:74:a2:af:fa:e6:a7:b4:71:21:f7:d9:6c:2f:
                    c5:33:d4:e2:fd:b1:93:8d:de:ff:2c:86:52:e9:84:
                    19:dd:ba:a6:0b:85:f4:64:ef:15:97:79:21:a9:da:
                    46:ef:b5:89:00:01:e0:6d:72:21:6b:ea:a3:7c:d1:
                    42:8a:26:ca:7c:f2:47:a8:8e:86:2b:a9:1b:61:66:
                    02:93:ab:57:cf:e4:7b:94:08:7f:71:62:f1:29:23:
                    35:a4:33:6c:e1:84:4c:c4:91:aa:45:b3:d4:a7:6b:
                    83:80:6b:ec:03:27:73:ff:10:20:1c:fd:aa:3d:79:
                    f7:4f:cf:c4:83:bd:4c:b1:5e:55:1c:f4:49:34:23:
                    c3:01:fc:25:b0:45:81:da:cc:10:84:66:e1:9b:c2:
                    4a:57
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:TRUE
            X509v3 Key Usage: 
                Digital Signature, Key Encipherment, Certificate Sign
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            Netscape Comment: 
                SSL Generated Certificate
            X509v3 Subject Key Identifier: 
                C6:C6:FD:7F:A3:EA:C1:50:0A:7E:33:1D:50:F7:E0:94:3F:93:EA:B7
            X509v3 Authority Key Identifier: 
                keyid:5B:5B:28:4B:13:37:60:B5:95:D4:5B:47:09:97:59:DF:16:63:AF:D9
                DirName:/C=DE/ST=STATE/L=CITY/O=ORG/OU=ORGUNIT/CN=RootCA
                serial:11:B5:B3:C6:0E:7B:13:28:C9:E6:28:3C:F1:40:25:6D:CB:14:EB:3C

    Signature Algorithm: sha384WithRSAEncryption
         92:16:c7:1b:6c:7a:f9:a1:dc:57:bd:24:45:26:0a:72:91:75:
         38:bc:f0:2c:d3:9f:ab:7a:bf:11:c0:1a:60:f3:5d:6a:ba:fe:
         f7:83:c4:21:f9:72:02:eb:47:85:16:6b:c9:38:58:6b:06:5f:
         c8:55:c1:ac:6e:9d:3c:ca:20:d1:94:15:d8:86:ed:b6:58:fc:
         56:81:15:8d:53:8f:62:da:5a:15:74:b0:78:41:da:fc:c1:69:
         fb:8d:cc:65:86:de:e5:79:f8:2d:53:1c:5c:c8:76:50:07:fe:
         f5:31:46:73:ba:e8:be:bb:f3:63:09:ae:f4:91:22:99:68:f6:
         82:b3:52:e5:92:4c:91:c8:12:b2:df:48:71:cc:ee:44:22:db:
         7e:52:97:5f:99:13:96:06:31:67:8c:00:c0:31:62:57:9a:aa:
         82:fe:e5:d9:56:07:fa:2a:15:fa:47:01:e4:ce:b0:98:fe:c0:
         ab:67:6d:7b:dc:30:d5:51:f5:13:7e:44:48:2a:d5:6f:4d:ab:
         22:0c:1a:45:bb:d3:36:37:aa:5c:f6:d3:6e:6e:d7:83:8c:3d:
         60:f5:b4:c9:67:f0:f3:a0:3a:6a:9e:7d:8c:e0:8e:75:9c:9c:
         9b:25:9b:cc:71:a2:53:4f:64:a5:8e:dd:18:7e:49:96:7d:d5:
         c0:c0:e5:2d
-----BEGIN CERTIFICATE-----
MIIETzCCAzegAwIBAgIUEbWzxg57EyjJ5ig88UAlbcsU6z0wDQYJKoZIhvcNAQEM
BQAwTTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQwwCgYDVQQKDANPUkcx
EDAOBgNVBAsMB09SR1VOSVQxDjAMBgNVBAMMBU9yZ0NhMB4XDTIzMTAwNjE1NDIz
MFoXDTI0MTEwOTE1NDIzMFowTjELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRF
MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlRlYW1D
QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIeI48qOivFeHrR4HTJ5
771RdPtAjYUBmKSzc/oY9V98bPtWrTDu39oZy9vS+FmLFVJqRsIcEk3tg6Vnl0ee
mJR44v3kfhhIEpIpVEmsu47e28IiN6lPDf85WsqYK/217OLhiCrPtjpgGxF0oq/6
5qe0cSH32WwvxTPU4v2xk43e/yyGUumEGd26pguF9GTvFZd5IanaRu+1iQAB4G1y
IWvqo3zRQoomynzyR6iOhiupG2FmApOrV8/ke5QIf3Fi8SkjNaQzbOGETMSRqkWz
1Kdrg4Br7AMnc/8QIBz9qj1590/PxIO9TLFeVRz0STQjwwH8JbBFgdrMEIRm4ZvC
SlcCAwEAAaOCASQwggEgMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgKkMB0GA1Ud
JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAoBglghkgBhvhCAQ0EGxYZU1NMIEdl
bmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUxsb9f6PqwVAKfjMdUPfglD+T
6rcwgZoGA1UdIwSBkjCBj4AUW1soSxM3YLWV1FtHCZdZ3xZjr9mhYaRfMF0xCzAJ
BgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFURTENMAsGA1UEBwwEQ0lUWTEMMAoGA1UE
CgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQDDAZSb290Q0GCFBG1s8YO
exMoyeYoPPFAJW3LFOs8MA0GCSqGSIb3DQEBDAUAA4IBAQCSFscbbHr5odxXvSRF
JgpykXU4vPAs05+rer8RwBpg811quv73g8Qh+XIC60eFFmvJOFhrBl/IVcGsbp08
yiDRlBXYhu22WPxWgRWNU49i2loVdLB4Qdr8wWn7jcxlht7lefgtUxxcyHZQB/71
MUZzuui+u/NjCa70kSKZaPaCs1LlkkyRyBKy30hxzO5EItt+UpdfmROWBjFnjADA
MWJXmqqC/uXZVgf6KhX6RwHkzrCY/sCrZ2173DDVUfUTfkRIKtVvTasiDBpFu9M2
N6pc9tNubteDjD1g9bTJZ/DzoDpqnn2M4I51nJybJZvMcaJTT2Sljt0YfkmWfdXA
wOUt
-----END CERTIFICATE-----
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            11:b5:b3:c6:0e:7b:13:28:c9:e6:28:3c:f1:40:25:6d:cb:14:eb:3c
        Signature Algorithm: sha384WithRSAEncryption
        Issuer: C = DE, ST = STATE, L = CITY, O = ORG, OU = ORGUNIT, CN = RootCA
        Validity
            Not Before: Oct  6 15:42:30 2023 GMT
            Not After : Feb 17 15:42:30 2025 GMT
        Subject: C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = OrgCa
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:c5:6a:66:7e:44:c7:79:2c:85:ab:72:48:b8:d3:
                    c2:a4:33:a9:ec:4d:4a:4c:9a:cb:f7:d8:a9:38:81:
                    70:12:f2:6c:b5:31:f4:f9:2b:5c:d3:e6:d1:d3:7e:
                    97:a7:ab:30:06:6a:82:13:15:3b:bf:1c:b5:9a:81:
                    c6:da:ab:8b:10:b7:3e:ec:21:63:72:fd:6e:cd:6e:
                    83:53:af:aa:d1:1e:66:79:42:03:50:aa:71:a5:6e:
                    ac:8f:5d:1a:b1:21:35:65:10:56:7f:fb:59:f7:f7:
                    3c:1c:41:1d:a3:bd:98:a5:df:a6:00:9a:9f:a9:f4:
                    f4:10:3f:1d:63:9e:dc:ab:44:3d:8a:2a:bc:70:7f:
                    56:e0:bd:ca:1b:45:54:94:72:db:12:02:22:c9:07:
                    f5:cf:60:6f:a8:b5:b0:cc:8e:16:25:33:21:f3:3a:
                    ac:7a:12:2c:f4:f6:25:55:be:98:4a:d0:cc:5a:25:
                    82:16:27:70:6b:d3:4d:f6:10:0f:2d:75:03:1f:90:
                    a9:31:28:24:78:c3:4a:af:54:69:46:a4:5c:c0:3a:
                    6b:94:5f:3e:b8:86:b9:40:ce:c1:0f:bf:de:5b:cf:
                    59:14:49:49:cb:d7:27:d5:d0:d2:14:b9:5b:4d:0a:
                    90:1c:e3:2b:8c:c4:d5:8d:a7:9c:db:2b:60:36:45:
                    e7:3d
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:TRUE
            X509v3 Key Usage: 
                Digital Signature, Key Encipherment, Certificate Sign
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            Netscape Comment: 
                SSL Generated Certificate
            X509v3 Subject Key Identifier: 
                5B:5B:28:4B:13:37:60:B5:95:D4:5B:47:09:97:59:DF:16:63:AF:D9
            X509v3 Authority Key Identifier: 
                keyid:33:82:33:48:FA:00:69:FE:13:84:27:0E:1D:98:F6:B4:79:31:CD:43
                DirName:/C=DE/ST=STATE/L=CITY/O=ORG/OU=ORGUNIT/CN=RootCA
                serial:03:5D:9E:F7:83:4A:B7:2A:C6:21:9A:5D:11:88:2B:AA:48:C7:5C:DF

    Signature Algorithm: sha384WithRSAEncryption
         9e:36:a2:c3:9f:7c:88:74:be:19:70:3f:bd:bf:44:93:86:1e:
         e7:72:96:66:62:d6:45:ee:c3:22:d8:09:13:2c:96:26:e8:dc:
         72:8d:6d:c4:51:68:32:58:ec:4b:45:1b:59:58:fb:ef:bc:91:
         c2:f3:ed:c6:4f:70:8b:e1:48:f7:7e:b4:b6:91:98:1d:a1:0e:
         e0:08:36:ff:d7:8f:79:d8:5d:76:f6:49:d7:c1:9e:24:58:dd:
         48:77:69:8e:80:82:ec:f5:5a:44:0d:b8:7d:5c:8e:ce:b0:1d:
         e7:3c:b4:73:10:e6:1b:9e:fb:45:42:34:64:98:58:a2:da:4b:
         b9:3f:df:61:c2:1e:25:f8:8e:84:3d:2c:e7:a9:43:54:2d:39:
         3b:9c:f9:9b:22:e1:37:dd:46:25:11:a1:c6:3a:60:18:56:56:
         8d:e0:99:31:8a:5b:ad:a5:4f:4b:b5:d4:cf:ca:91:93:1b:d4:
         41:16:56:85:fd:99:df:0d:48:c1:0c:af:4a:60:e0:d2:9e:9b:
         18:81:58:fe:54:f2:42:bc:60:70:d4:f8:0c:70:4a:b3:3f:90:
         0b:63:f3:1b:b1:2e:40:c2:ef:59:ab:49:9b:26:22:c2:09:8e:
         ec:39:d0:95:8a:fc:af:46:f9:70:09:43:6c:b4:6e:ae:f1:8e:
         c8:c4:71:e7
-----BEGIN CERTIFICATE-----
MIIEXjCCA0agAwIBAgIUEbWzxg57EyjJ5ig88UAlbcsU6zwwDQYJKoZIhvcNAQEM
BQAwXTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQ0wCwYDVQQHDARDSVRZ
MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlJvb3RD
QTAeFw0yMzEwMDYxNTQyMzBaFw0yNTAyMTcxNTQyMzBaME0xCzAJBgNVBAYTAkRF
MQ4wDAYDVQQIDAVTVEFURTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklU
MQ4wDAYDVQQDDAVPcmdDYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AMVqZn5Ex3kshatySLjTwqQzqexNSkyay/fYqTiBcBLybLUx9PkrXNPm0dN+l6er
MAZqghMVO78ctZqBxtqrixC3PuwhY3L9bs1ug1OvqtEeZnlCA1CqcaVurI9dGrEh
NWUQVn/7Wff3PBxBHaO9mKXfpgCan6n09BA/HWOe3KtEPYoqvHB/VuC9yhtFVJRy
2xICIskH9c9gb6i1sMyOFiUzIfM6rHoSLPT2JVW+mErQzFolghYncGvTTfYQDy11
Ax+QqTEoJHjDSq9UaUakXMA6a5RfPriGuUDOwQ+/3lvPWRRJScvXJ9XQ0hS5W00K
kBzjK4zE1Y2nnNsrYDZF5z0CAwEAAaOCASQwggEgMAwGA1UdEwQFMAMBAf8wCwYD
VR0PBAQDAgKkMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAoBglghkgB
hvhCAQ0EGxYZU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUW1so
SxM3YLWV1FtHCZdZ3xZjr9kwgZoGA1UdIwSBkjCBj4AUM4IzSPoAaf4ThCcOHZj2
tHkxzUOhYaRfMF0xCzAJBgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFURTENMAsGA1UE
BwwEQ0lUWTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQD
DAZSb290Q0GCFANdnveDSrcqxiGaXRGIK6pIx1zfMA0GCSqGSIb3DQEBDAUAA4IB
AQCeNqLDn3yIdL4ZcD+9v0SThh7ncpZmYtZF7sMi2AkTLJYm6NxyjW3EUWgyWOxL
RRtZWPvvvJHC8+3GT3CL4Uj3frS2kZgdoQ7gCDb/14952F129knXwZ4kWN1Id2mO
gILs9VpEDbh9XI7OsB3nPLRzEOYbnvtFQjRkmFii2ku5P99hwh4l+I6EPSznqUNU
LTk7nPmbIuE33UYlEaHGOmAYVlaN4JkxilutpU9LtdTPypGTG9RBFlaF/ZnfDUjB
DK9KYODSnpsYgVj+VPJCvGBw1PgMcEqzP5ALY/MbsS5Awu9Zq0mbJiLCCY7sOdCV
ivyvRvlwCUNstG6u8Y7IxHHn
-----END CERTIFICATE-----
0707010000008E000081A4000003E80000006400000001660688CE000006A8000000000000000000000000000000000000003C00000000uyuni-tools/mgradm/shared/ssl/testdata/chain2/spacewalk.key-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4P0faE/70e6//
dfwL3ecm1jR7GTOAfbkgQBe4NKSAOky7JQyKQGVHMgSv7y22l3BmHiMosI+Y1PAs
tsBAaikGwo1egVtgZlRUn/13rrRiY4fxW/uqQcyCFhA+NZ2ZmGPBrSx7LQIOCq8d
dW1cRME9qCilpFM1EFtYqKtUd6309OdaUV91bwU3/VVWok0uOlg6pNatIG1Pfh2i
g5SibAy4A7o5VQWTrXyfehKZKD5TnTqDvEo+bi5S5mOi+ufZEpAsW3hSNJIZGawo
hMMlT4/5DWTv6+S8zYeJHHQBb+IaeJLiLhXQjiuUaW2H9JHxXfNHc5Xj1oCHkxVq
966vg7ZVAgMBAAECggEAErkNgGX5Sdda2GshKISNdYcdcKfsMaG1Awe4UVX6JHSo
MPlQF6l5ET3ON6Gmw9AKUjo8SOl+QiHbYTPWAAW5sw/opUKgakCjz7CtZXDZsEjc
euSlw5Spp0t+LZAtunq/omIKa970P0CLMINrEE4FVBJnRQPYl8MYgT8sn+oEgaiI
J0p1MaxvvLWJfCF9niFWevCxjWwFNP5nYA7XdfdG8yKO7AXIlE3Xte174juTQ5+Q
neAEYnh2bp+uqEgfgvhp830NsBmegvqn7USSUqWXO++aTEwlDv70t5YWtD6GvSu2
8SWEPdAsLZkH1tzD+0jgMhdiJdcsY48fSnOxP1LHSQKBgQDI+SBLwtvAtEHI1lY7
LQ+lonJSNY/MtFEkPBAgDtU97PzF7ucoQdYitoLOyAu2d6TN7ze/H8crKjLSbKXn
X5DcID+7fLWbAeF4jDUfB9cm/zMkywxHcWL3K+aHzD08NVdTcGpIxViUZ/7ekBOr
9l+32+tRkYLumH9roUsSnLN0DQKBgQDqscbR8OtzwGi2ic/cjJvyZ1Vkbr19yeea
2WM0RiSO3vU7NY7aBSWkoHOAECKdKqJ0J7VhhMYYP+LKGFQbQQge+LmQpDRIJaM9
tq5QpOW6YYpwvGLwsbHSwfz93iRR+SO9+X9mx/FeoyOv9qKBvxv3UzdNpO5W5BKt
3oJiqbVRaQKBgQCC8sB2XNru7wTGJdI98Jh3ZidzJW8zBHKyV2hyWvfax6XUGlwH
wQ4TxDPrJDFtjPuXKz15jO0rVO2UajKXVY9/vouIUDPMcidFcqXSODuaL0JVwO+Z
RWokfzhQV2W261KhDWhTTjLvT+ujfOE0dO3dULA9j8BuUnMD4C6YS/4pqQKBgQC3
2oSyOlV43CYruVIIqG4SOzj98HKpc93nxJyeesRw1+CsfYxm5tlSWg+hJwK2tIuH
CwRgXK8CmCmFwAFDSHKgMKDN2pTKYBG9arqrmkIM/BSDtFCd1dZEEIusJLW3McD6
NdXEIqXHSW3PjxpHIfs6iQot3SKJFyo64rCpseDE4QKBgHHp6c9UEJjq+IYT6hXg
kvxS87ZWWVqFRAjS93WRS7ZllFyX9+9GfUAEaOrArJf8gvVl+8QOqVn9spjUoV0o
VIWMPlh9VRS9nQGYNTTg3vYRRCdNE0SGwNg4CL7oW1kvoPOVMk7nST/9AnGAMpkF
LY7A0vS56vx3wZuWfPbqcK1s
-----END PRIVATE KEY-----
0707010000008F000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002400000000uyuni-tools/mgradm/shared/templates07070100000090000081A4000003E80000006400000001660688CE000002D5000000000000000000000000000000000000003700000000uyuni-tools/mgradm/shared/templates/inspectTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"

	"github.com/uyuni-project/uyuni-tools/shared/types"
)

const inspectTemplate = `#!/bin/bash
# inspect.sh, generated by mgradm
{{- range .Param }}
echo "{{ .Variable }}=$({{ .CLI }})" >> {{ $.OutputFile }}
{{- end }}
exit 0
`

// InspectTemplateData represents information used to create inspect script.
type InspectTemplateData struct {
	Param      []types.InspectData
	OutputFile string
}

// Render will create inspect script.
func (data InspectTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("inspect").Parse(inspectTemplate))
	return t.Execute(wr, data)
}
07070100000091000081A4000003E80000006400000001660688CE00000811000000000000000000000000000000000000003600000000uyuni-tools/mgradm/shared/templates/issuerTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

// Deploy self-signed issuer or CA Certificate and key.
const issuerTemplate = `{{if and .Certificate .Key -}}
apiVersion: v1
kind: Secret
type: kubernetes.io/tls
metadata:
  name: uyuni-ca
  namespace: {{ .Namespace }}
data:
  ca.crt: {{ .RootCa }}
  tls.crt: {{ .Certificate }}
  tls.key: {{ .Key }}
{{- else }}
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: uyuni-issuer
  namespace: {{ .Namespace }}
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: uyuni-ca
  namespace: {{ .Namespace }}
spec:
  isCA: true
{{- if or .Country .State .City .Org .OrgUnit }}
  subject:
	{{- if .Country }}
    countries: ["{{ .Country }}"]
	{{- end }}
	{{- if .State }}
    provinces: ["{{ .State }}"]
	{{- end }}
	{{- if .City }}
    localities: ["{{ .City }}"]
	{{- end }}
	{{- if .Org }}
    organizations: ["{{ .Org }}"]
	{{- end }}
	{{- if .OrgUnit }}
    organizationalUnits: ["{{ .OrgUnit }}"]
	{{- end }}
{{- end }}
{{- if .Email }}
  emailAddresses:
    - {{ .Email }}
{{- end }}
  commonName: {{ .Fqdn }}
  dnsNames:
    - {{ .Fqdn }}
  secretName: uyuni-ca
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: uyuni-issuer
    kind: Issuer
    group: cert-manager.io
{{- end }}
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: uyuni-ca-issuer
  namespace: {{ .Namespace }}
spec:
  ca:
    secretName:
      uyuni-ca
`

// IssuerTemplateData represents information used to create issuer file.
type IssuerTemplateData struct {
	Namespace   string
	Country     string
	State       string
	City        string
	Org         string
	OrgUnit     string
	Email       string
	Fqdn        string
	RootCa      string
	Certificate string
	Key         string
}

// Render creates issuer file.
func (data IssuerTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("issuer").Parse(issuerTemplate))
	return t.Execute(wr, data)
}
07070100000092000081A4000003E80000006400000001660688CE0000049D000000000000000000000000000000000000003E00000000uyuni-tools/mgradm/shared/templates/mgrSetupScriptTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

const mgrSetupScriptTemplate = `#!/bin/sh
{{- range $name, $value := .Env }}
export {{ $name }}={{ $value }}
{{- end }}

{{- if .DebugJava }}
echo 'JAVA_OPTS=" $JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=*:8003,server=y,suspend=n" ' >> /etc/tomcat/conf.d/remote_debug.conf
echo 'JAVA_OPTS=" $JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=*:8001,server=y,suspend=n" ' >> /etc/rhn/taskomatic.conf
echo 'JAVA_OPTS=" $JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=*:8002,server=y,suspend=n" ' >> /usr/share/rhn/config-defaults/rhn_search_daemon.conf
{{- end }}

/usr/lib/susemanager/bin/mgr-setup -s -n

# clean before leaving
rm $0`

// MgrSetupScriptTemplateData represents information used to create setup script.
type MgrSetupScriptTemplateData struct {
	Env       map[string]string
	DebugJava bool
}

// Render will create setup script.
func (data MgrSetupScriptTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("script").Parse(mgrSetupScriptTemplate))
	return t.Execute(wr, data)
}
07070100000093000081A4000003E80000006400000001660688CE000015A2000000000000000000000000000000000000003D00000000uyuni-tools/mgradm/shared/templates/migrateScriptTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"

	"github.com/uyuni-project/uyuni-tools/shared/types"
)

const migrationScriptTemplate = `#!/bin/bash
set -e
SSH_CONFIG=""
if test -e /tmp/ssh_config; then
  SSH_CONFIG="-F /tmp/ssh_config"
fi
SSH="ssh -A $SSH_CONFIG "
SCP="scp -A $SSH_CONFIG "

echo "Stopping spacewalk service..."
$SSH {{ .SourceFqdn }} "spacewalk-service stop ; systemctl start postgresql.service"

$SSH {{ .SourceFqdn }} \
 "echo \"COPY (SELECT MIN(CONCAT(org_id, '-', label)) AS target, base_path FROM rhnKickstartableTree GROUP BY base_path) TO STDOUT WITH CSV;\" \
 |spacewalk-sql --select-mode - " > distros

echo "Stopping posgresql service..."
$SSH {{ .SourceFqdn }} "systemctl stop postgresql.service"

while IFS="," read -r target path ; do
    echo "-/ $path"
done < distros > exclude_list

# exclude all config files which already exist and are not marked noreplace
rpm -qa --qf '[%{fileflags},%{filenames}\n]' |grep ",/etc/" | while IFS="," read -r flags path ; do
    # config(noreplace) is 1<<4 (from lib/rpmlib.h)
    if [ $(( $flags & 16 )) -eq 0 -a -f "$path" ] ; then
        echo "-/ $path" >> exclude_list
    fi
done

# exclude schema migration files
echo "-/ /etc/sysconfig/rhn/reportdb-schema-upgrade" >> exclude_list
echo "-/ /etc/sysconfig/rhn/schema-upgrade" >> exclude_list


for folder in {{ range .Volumes }}{{ .MountPath }} {{ end }};
do
  if $SSH {{ .SourceFqdn }} test -e $folder; then
    echo "Copying $folder..."
    rsync -e "$SSH" --rsync-path='sudo rsync' -avz -f "merge exclude_list" {{ .SourceFqdn }}:$folder/ $folder;
  else
    echo "Skipping missing $folder..."
  fi
done;

sed -i -e 's|appBase="webapps"|appBase="/usr/share/susemanager/www/tomcat/webapps"|' /etc/tomcat/server.xml
sed -i -e 's|DocumentRoot\s*"/srv/www/htdocs"|DocumentRoot "/usr/share/susemanager/www/htdocs"|' /etc/apache2/vhosts.d/vhost-ssl.conf

echo "Migrating auto-installable distributions..."
while IFS="," read -r target path ; do
  if $SSH -A {{ .SourceFqdn }} test -e $path; then
    echo "Copying distribution $target from $path"
    mkdir -p "/srv/www/distributions/$target"
    rsync -e "$SSH" --rsync-path='sudo rsync' -avz "{{ .SourceFqdn }}:$path/" "/srv/www/distributions/$target"
  else
    echo "Skipping missing distribution $path..."
  fi
done < distros

rm -f /srv/www/htdocs/pub/RHN-ORG-TRUSTED-SSL-CERT;
ln -s /etc/pki/trust/anchors/LOCAL-RHN-ORG-TRUSTED-SSL-CERT /srv/www/htdocs/pub/RHN-ORG-TRUSTED-SSL-CERT;

echo "Extracting time zone..."
$SSH {{ .SourceFqdn }} timedatectl show -p Timezone >/var/lib/uyuni-tools/data

echo "Extracting postgresql versions..."
echo "new_pg_version=$(rpm -qa --qf '%{VERSION}\n' 'name=postgresql[0-8][0-9]-server'  | cut -d. -f1 | sort -n | tail -1)" >> /var/lib/uyuni-tools/data
echo "old_pg_version=$(cat /var/lib/pgsql/data/PG_VERSION)" >> /var/lib/uyuni-tools/data

echo "Altering configuration for domain resolution..."
sed 's/report_db_host = {{ .SourceFqdn }}/report_db_host = localhost/' -i /etc/rhn/rhn.conf;
sed 's/server\.jabber_server/java\.hostname/' -i /etc/rhn/rhn.conf;
sed 's/client_use_localhost: false/client_use_localhost: true/' -i /etc/cobbler/settings.yaml;

echo "Altering configuration for container environment..."
sed 's/address=[^:]*:/address=*:/' -i /etc/rhn/taskomatic.conf;

if test ! -f /etc/tomcat/conf.d/remote_debug.conf -a -f /etc/sysconfig/tomcat; then
  mv /etc/sysconfig/tomcat /etc/tomcat/conf.d/remote_debug.conf
fi

sed 's/address=[^:]*:/address=*:/' -i /etc/tomcat/conf.d/remote_debug.conf

{{ if .Kubernetes }}
echo 'server.no_ssl = 1' >> /etc/rhn/rhn.conf;
echo "Extracting SSL certificate and authority"
extractedSSL=
if test -d /root/ssl-build; then
  # We may have an old unused ssl-build folder, check if the CA matches the deployed one
  buildCaFingerprint=
  if test -e /root/ssl-build/RHN-ORG-TRUSTED-SSL-CERT; then
    buildCaFingerprint=$(openssl x509 -in /root/ssl-build/RHN-ORG-TRUSTED-SSL-CERT -noout -fingerprint)
  fi
  caFingerprint=$(openssl x509 -in /etc/pki/trust/anchors/LOCAL-RHN-ORG-TRUSTED-SSL-CERT -noout -fingerprint)

  if test "$buildCaFingerprint" == "$caFingerprint"; then
    echo "Extracting SSL Root CA key..."
    # Extract the SSL CA certificate and key.
    # The server certificate will be auto-generated by cert-manager using it, so no need to copy it.
    cp /root/ssl-build/RHN-ORG-PRIVATE-SSL-KEY /var/lib/uyuni-tools/

    extractedSSL="1"
  fi
fi

# This Root CA file is common to both cases
cp /etc/pki/trust/anchors/LOCAL-RHN-ORG-TRUSTED-SSL-CERT /var/lib/uyuni-tools/RHN-ORG-TRUSTED-SSL-CERT

if test "extractedSSL" != "1"; then
  # For third party certificates, the CA chain is in the certificate file.
  $SCP {{ .SourceFqdn }}:/etc/pki/tls/private/spacewalk.key /var/lib/uyuni-tools/
  $SCP {{ .SourceFqdn }}:/etc/pki/tls/certs/spacewalk.crt /var/lib/uyuni-tools/
fi

echo "Removing useless ssl-build folder..."
rm -rf /root/ssl-build

# The content of this folder will be a RO mount from a configmap
rm /etc/pki/trust/anchors/*
{{ end }}

echo "DONE"`

// MigrateScriptTemplateData represents migration information used to create migration script.
type MigrateScriptTemplateData struct {
	Volumes    []types.VolumeMount
	SourceFqdn string
	Kubernetes bool
}

// Render will create migration script.
func (data MigrateScriptTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("script").Parse(migrationScriptTemplate))
	return t.Execute(wr, data)
}
07070100000094000081A4000003E80000006400000001660688CE000008F6000000000000000000000000000000000000004300000000uyuni-tools/mgradm/shared/templates/pgsqlFinalizeScriptTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

const postgresFinalizeScriptTemplate = `#!/bin/bash
set -e

{{ if .RunAutotune }}
echo "Running smdba system-check autotuning..."
smdba system-check autotuning
{{ end }}
echo "Starting Postgresql..."
su -s /bin/bash - postgres -c "/usr/share/postgresql/postgresql-script start"
{{ if .RunReindex }}
echo "Reindexing database. This may take a while, please do not cancel it!"
database=$(sed -n "s/^\s*db_name\s*=\s*\([^ ]*\)\s*$/\1/p" /etc/rhn/rhn.conf)
spacewalk-sql --select-mode - <<<"REINDEX DATABASE \"${database}\";"
{{ end }}

{{ if .RunSchemaUpdate }}
echo "Schema update..."
/usr/sbin/spacewalk-startup-helper check-database
{{ end }}

{{ if .RunDistroMigration }}
echo "Updating auto-installable distributions..."
spacewalk-sql --select-mode - <<EOT
SELECT MIN(CONCAT(org_id, '-', label)) AS target, base_path INTO TEMP TABLE dist_map FROM rhnKickstartableTree GROUP BY base_path;
UPDATE rhnKickstartableTree SET base_path = CONCAT('/srv/www/distributions/', target)
    from dist_map WHERE dist_map.base_path = rhnKickstartableTree.base_path;
DROP TABLE dist_map;
EOT
{{ end }}

echo "Schedule a system list update task..."
spacewalk-sql --select-mode - <<EOT
insert into rhnTaskQueue (id, org_id, task_name, task_data)
SELECT nextval('rhn_task_queue_id_seq'), 1, 'update_system_overview', s.id
from rhnserver s
where not exists (select 1 from rhntaskorun r join rhntaskotemplate t on r.template_id = t.id
join rhntaskobunch b on t.bunch_id = b.id where b.name='update-system-overview-bunch' limit 1);
EOT


echo "Stopping Postgresql..."
su -s /bin/bash - postgres -c "/usr/share/postgresql/postgresql-script stop"
echo "DONE"
`

// FinalizePostgresTemplateData represents information used to create PostgreSQL migration script.
type FinalizePostgresTemplateData struct {
	RunAutotune        bool
	RunReindex         bool
	RunSchemaUpdate    bool
	RunDistroMigration bool
	Kubernetes         bool
}

// Render will create script for finalizing PostgreSQL upgrade.
func (data FinalizePostgresTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("script").Parse(postgresFinalizeScriptTemplate))
	return t.Execute(wr, data)
}
07070100000095000081A4000003E80000006400000001660688CE000008D3000000000000000000000000000000000000004900000000uyuni-tools/mgradm/shared/templates/pgsqlVersionUpgradeScriptTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

const postgreSQLVersionUpgradeScriptTemplate = `#!/bin/bash
set -e
echo "PostgreSQL version upgrade"

OLD_VERSION={{ .OldVersion }}
NEW_VERSION={{ .NewVersion }}
FAST_UPGRADE=--link

echo "Testing presence of postgresql$NEW_VERSION..."
test -d /usr/lib/postgresql$NEW_VERSION/bin
echo "Testing presence of postgresql$OLD_VERSION..."
test -d /usr/lib/postgresql$OLD_VERSION/bin

echo "Create a backup at /var/lib/pgsql/data-pg$OLD_VERSION..."
mv /var/lib/pgsql/data /var/lib/pgsql/data-pg$OLD_VERSION
echo "Create new database directory..."
mkdir -p /var/lib/pgsql/data
chown -R postgres:postgres /var/lib/pgsql
echo "Enforce key permission"
chown -R postgres:postgres /etc/pki/tls/private/pg-spacewalk.key
chown -R postgres:postgres /etc/pki/tls/certs/spacewalk.crt

echo "Initialize new postgresql $NEW_VERSION database..."
. /etc/sysconfig/postgresql 2>/dev/null # Load locale for SUSE
PGHOME=$(getent passwd postgres | awk -F: '{print $6}')
#. $PGHOME/.i18n 2>/dev/null # Load locale for Enterprise Linux
if [ -z $POSTGRES_LANG ]; then
    POSTGRES_LANG="en_US.UTF-8"
    [ ! -z $LC_CTYPE ] && POSTGRES_LANG=$LC_CTYPE
fi

echo "Running initdb using postgres user"
echo "Any suggested command from the console should be run using postgres user"
su -s /bin/bash - postgres -c "initdb -D /var/lib/pgsql/data --locale=$POSTGRES_LANG"
echo "Successfully initialized new postgresql $NEW_VERSION database."
su -s /bin/bash - postgres -c "pg_upgrade --old-bindir=/usr/lib/postgresql$OLD_VERSION/bin --new-bindir=/usr/lib/postgresql$NEW_VERSION/bin --old-datadir=/var/lib/pgsql/data-pg$OLD_VERSION --new-datadir=/var/lib/pgsql/data $FAST_UPGRADE"

echo "DONE"`

// PostgreSQLVersionUpgradeTemplateData represents information used to create PostgreSQL migration script.
type PostgreSQLVersionUpgradeTemplateData struct {
	OldVersion string
	NewVersion string
	Kubernetes bool
}

// Render will create PostgreSQL migration script.
func (data PostgreSQLVersionUpgradeTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("script").Parse(postgreSQLVersionUpgradeScriptTemplate))
	return t.Execute(wr, data)
}
07070100000096000081A4000003E80000006400000001660688CE000003D9000000000000000000000000000000000000004100000000uyuni-tools/mgradm/shared/templates/postUpgradeScriptTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

const postUpgradeScriptTemplate = `#!/bin/bash
{{ if .CobblerHost }}
sed 's/cobbler\.host.*/cobbler\.host = {{ .CobblerHost }}/' -i /etc/rhn/rhn.conf;
grep uyuni_authentication_endpoint /etc/cobbler/settings.yaml
if [ $? -eq 1 ]; then
	echo 'uyuni_authentication_endpoint: "http://localhost"' >> /etc/cobbler/settings.yaml
else
	sed 's/uyuni_authentication_endpoint.*/spacewalk_authentication_endpoint: http:\/\/localhost/' -i /etc/cobbler/settings.yaml;
fi
{{ end }}
`

// PostUpgradeTemplateData represents information used to create post upgrade.
type PostUpgradeTemplateData struct {
	CobblerHost string
}

// Render will create script for finalizing PostgreSQL upgrade.
func (data PostUpgradeTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("script").Parse(postUpgradeScriptTemplate))
	return t.Execute(wr, data)
}
07070100000097000081A4000003E80000006400000001660688CE00000838000000000000000000000000000000000000003700000000uyuni-tools/mgradm/shared/templates/serviceTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"

	"github.com/uyuni-project/uyuni-tools/shared/types"
)

const serviceTemplate = `# uyuni-server.service, generated by mgradm
# Use an uyuni-server.service.d/local.conf file to override

[Unit]
Description=Uyuni server image container service
Wants=network.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Environment=TZ={{ .Timezone }}
Restart=on-failure
ExecStartPre=/bin/rm -f %t/uyuni-server.pid %t/%n.ctr-id
ExecStartPre=/usr/bin/podman rm --ignore --force -t 10 {{ .NamePrefix }}-server
ExecStart=/usr/bin/podman run \
	--conmon-pidfile %t/uyuni-server.pid \
	--cidfile=%t/%n.ctr-id \
	--cgroups=no-conmon \
	--shm-size=0 \
	--shm-size-systemd=0 \
	--sdnotify=conmon \
	-d \
	--name {{ .NamePrefix }}-server \
	--hostname {{ .NamePrefix }}-server \
	{{ .Args }} \
	{{- range .Ports }}
	-p {{ .Exposed }}:{{ .Port }}{{if .Protocol}}/{{ .Protocol }}{{end}} \
	{{- end }}
	{{- range .Volumes }}
	-v {{ .Name }}:{{ .MountPath }} \
	{{- end }}
	-e TZ=${TZ} \
	--network {{ .Network }} \
	${UYUNI_IMAGE}
ExecStop=/usr/bin/podman exec \
    uyuni-server \
    /bin/bash -c 'spacewalk-service stop && systemctl stop postgresql'
ExecStop=/usr/bin/podman stop \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
	-f \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id

PIDFile=%t/uyuni-server.pid
TimeoutStopSec=180
TimeoutStartSec=900
Type=forking

[Install]
WantedBy=multi-user.target default.target
`

// PodmanServiceTemplateData POD information to create systemd file.
type PodmanServiceTemplateData struct {
	Volumes    []types.VolumeMount
	NamePrefix string
	Args       string
	Ports      []types.PortMap
	Timezone   string
	Image      string
	Network    string
}

// Render will create the systemd configuration file.
func (data PodmanServiceTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("service").Parse(serviceTemplate))
	return t.Execute(wr, data)
}
07070100000098000081A4000003E80000006400000001660688CE0000034F000000000000000000000000000000000000003100000000uyuni-tools/mgradm/shared/templates/tlsSecret.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

// Deploy self-signed issuer or CA Certificate and key.
const tlsSecretTemplate = `apiVersion: v1
kind: Secret
type: kubernetes.io/tls
metadata:
  name: {{ .Name }}
  namespace: {{ .Namespace }}
data:
  ca.crt: {{ .RootCa }}
  tls.crt: {{ .Certificate }}
  tls.key: {{ .Key }}
`

// TlsSecretTemplateData contains information to create secret configuration file.
type TlsSecretTemplateData struct {
	Name        string
	Namespace   string
	RootCa      string
	Certificate string
	Key         string
}

// Render creates secret configuration file.
func (data TlsSecretTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("secret").Parse(tlsSecretTemplate))
	return t.Execute(wr, data)
}
07070100000099000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002000000000uyuni-tools/mgradm/shared/utils0707010000009A000081A4000003E80000006400000001660688CE00000BB6000000000000000000000000000000000000002D00000000uyuni-tools/mgradm/shared/utils/cmd_utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"fmt"
	"path"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

var defaultImage = path.Join(utils.DefaultNamespace, "server")

// HelmFrags stores Uyuni and Cert Manager Helm information.
type HelmFlags struct {
	Uyuni       types.ChartFlags
	CertManager types.ChartFlags
}

// SslCertFlags can store SSL Certs information.
type SslCertFlags struct {
	Cnames   []string `mapstructure:"cname"`
	Country  string
	State    string
	City     string
	Org      string
	OU       string
	Password string
	Email    string
	Ca       ssl.CaChain
	Server   ssl.SslPair
}

// UseExisting return true if existing SSL Cert can be used.
func (f *SslCertFlags) UseExisting() bool {
	return f.Server.Cert != "" && f.Server.Key != "" && f.Ca.Root != ""
}

// Checks that all the required flags are passed if using 3rd party certificates.
func (f *SslCertFlags) CheckParameters() {
	if !f.UseExisting() && (f.Server.Cert != "" || f.Server.Key != "" || f.Ca.Root != "") {
		log.Fatal().Msg("Server certificate, key and root CA need to be all provided")
	}
}

// AddHelmInstallFlag add Helm install flags to a command.
func AddHelmInstallFlag(cmd *cobra.Command) {
	defaultChart := fmt.Sprintf("oci://%s/server-helm", utils.DefaultNamespace)

	cmd.Flags().String("helm-uyuni-namespace", "default", "Kubernetes namespace where to install uyuni")
	cmd.Flags().String("helm-uyuni-chart", defaultChart, "URL to the uyuni helm chart")
	cmd.Flags().String("helm-uyuni-version", "", "Version of the uyuni helm chart")
	cmd.Flags().String("helm-uyuni-values", "", "Path to a values YAML file to use for Uyuni helm install")
	cmd.Flags().String("helm-certmanager-namespace", "cert-manager", "Kubernetes namespace where to install cert-manager")
	cmd.Flags().String("helm-certmanager-chart", "", "URL to the cert-manager helm chart. To be used for offline installations")
	cmd.Flags().String("helm-certmanager-version", "", "Version of the cert-manager helm chart")
	cmd.Flags().String("helm-certmanager-values", "", "Path to a values YAML file to use for cert-manager helm install")
}

// AddimageFlag add Image flags to a command.
func AddImageFlag(cmd *cobra.Command) {
	cmd.Flags().String("image", defaultImage, "Image")
	cmd.Flags().String("tag", utils.DefaultTag, "Tag Image")

	utils.AddPullPolicyFlag(cmd)
}

// AddMigrationImageFlag add Migration Image flags to a command.
func AddMigrationImageFlag(cmd *cobra.Command) {
	cmd.Flags().String("migration-image", "", "Migration image")
	cmd.Flags().String("migration-tag", utils.DefaultTag, "Migration image tag")
	cmd.Flags().String("migration-pullPolicy", "IfNotPresent",
		"set whether to pull the migrattion images or not. The value can be one of 'Never', 'IfNotPresent' or 'Always'")
}
0707010000009B000081A4000003E80000006400000001660688CE00002806000000000000000000000000000000000000002800000000uyuni-tools/mgradm/shared/utils/exec.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/viper"
	"github.com/uyuni-project/uyuni-tools/mgradm/shared/templates"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// InspectScriptFilename is the inspect script basename.
var InspectScriptFilename = "inspect.sh"

var inspectValues = []types.InspectData{
	types.NewInspectData("uyuni_release", "cat /etc/*release | grep 'Uyuni release' | cut -d ' ' -f3 || true"),
	types.NewInspectData("suse_manager_release", "cat /etc/*release | grep 'SUSE Manager release' | cut -d ' ' -f4 || true"),
	types.NewInspectData("fqdn", "cat /etc/rhn/rhn.conf 2>/dev/null | grep 'java.hostname' | cut -d' ' -f3 || true"),
	types.NewInspectData("image_pg_version", "rpm -qa --qf '%{VERSION}\\n' 'name=postgresql[0-8][0-9]-server'  | cut -d. -f1 | sort -n | tail -1 || true"),
	types.NewInspectData("current_pg_version", "(test -e /var/lib/pgsql/data/PG_VERSION && cat /var/lib/pgsql/data/PG_VERSION) || true"),
	types.NewInspectData("registration_info", "transactional-update --quiet register --status 2>/dev/null || true"),
	types.NewInspectData("scc_username", "cat /etc/zypp/credentials.d/SCCcredentials | grep username | cut -d= -f2 || true"),
	types.NewInspectData("scc_password", "cat /etc/zypp/credentials.d/SCCcredentials | grep password | cut -d= -f2 || true"),
}

// InspectOutputFile represents the directory and the basename where the inspect values are stored.
var InspectOutputFile = types.InspectFile{
	Directory: "/var/lib/uyuni-tools",
	Basename:  "data",
}

// ExecCommand execute commands passed as argument in the current system.
func ExecCommand(logLevel zerolog.Level, cnx *shared.Connection, args ...string) error {
	podName, err := cnx.GetPodName()
	if err != nil {
		return fmt.Errorf("execCommand failed %s", err)
	}

	commandArgs := []string{"exec", podName}

	command, err := cnx.GetCommand()
	if err != nil {
		log.Fatal().Err(err)
	}

	if command == "kubectl" {
		commandArgs = append(commandArgs, "-c", "uyuni", "--")
	}

	commandArgs = append(commandArgs, "sh", "-c", strings.Join(args, " "))

	runCmd := exec.Command(command, commandArgs...)
	logger := utils.OutputLogWriter{Logger: log.Logger, LogLevel: logLevel}
	runCmd.Stdout = logger
	runCmd.Stderr = logger
	return runCmd.Run()
}

// GeneratePgsqlVersionUpgradeScript generates the PostgreSQL version upgrade script.
func GeneratePgsqlVersionUpgradeScript(scriptDir string, oldPgVersion string, newPgVersion string, kubernetes bool) (string, error) {
	data := templates.PostgreSQLVersionUpgradeTemplateData{
		OldVersion: oldPgVersion,
		NewVersion: newPgVersion,
		Kubernetes: kubernetes,
	}

	scriptName := "pgsqlVersionUpgrade.sh"
	scriptPath := filepath.Join(scriptDir, scriptName)
	if err := utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil {
		return "", fmt.Errorf("failed to generate %s", scriptName)
	}
	return scriptName, nil
}

// GenerateFinalizePostgresScript generates the script to finalize PostgreSQL upgrade.
func GenerateFinalizePostgresScript(scriptDir string, RunAutotune bool, RunReindex bool, RunSchemaUpdate bool, RunDistroMigration bool, kubernetes bool) (string, error) {
	data := templates.FinalizePostgresTemplateData{
		RunAutotune:        RunAutotune,
		RunReindex:         RunReindex,
		RunSchemaUpdate:    RunSchemaUpdate,
		RunDistroMigration: RunDistroMigration,
		Kubernetes:         kubernetes,
	}

	scriptName := "pgsqlFinalize.sh"
	scriptPath := filepath.Join(scriptDir, scriptName)
	if err := utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil {
		return "", fmt.Errorf("failed to generate %s", scriptName)
	}
	return scriptName, nil
}

// GeneratePostUpgradeScript generates the script to be run after upgrade.
func GeneratePostUpgradeScript(scriptDir string, cobblerHost string) (string, error) {
	data := templates.PostUpgradeTemplateData{
		CobblerHost: cobblerHost,
	}

	scriptName := "postUpgrade.sh"
	scriptPath := filepath.Join(scriptDir, scriptName)
	if err := utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil {
		return "", fmt.Errorf("failed to generate %s", scriptName)
	}
	return scriptName, nil
}

// ReadContainerData returns values used to perform migration.
func ReadContainerData(scriptDir string) (string, string, string, error) {
	data, err := os.ReadFile(filepath.Join(scriptDir, "data"))
	if err != nil {
		return "", "", "", errors.New("failed to read data extracted from source host")
	}
	viper.SetConfigType("env")
	if err := viper.ReadConfig(bytes.NewBuffer(data)); err != nil {
		return "", "", "", fmt.Errorf("cannot read config: %s", err)
	}
	if len(viper.GetString("Timezone")) <= 0 {
		return "", "", "", errors.New("cannot retrieve timezone")
	}
	if len(viper.GetString("old_pg_version")) <= 0 {
		return "", "", "", errors.New("cannot retrieve source PostgreSQL version")
	}
	if len(viper.GetString("new_pg_version")) <= 0 {
		return "", "", "", errors.New("cannot retrieve image PostgreSQL version")
	}
	return viper.GetString("Timezone"), viper.GetString("old_pg_version"), viper.GetString("new_pg_version"), nil
}

// RunMigration execute the migration script.
func RunMigration(cnx *shared.Connection, tmpPath string, scriptName string) error {
	log.Info().Msg("Migrating server")
	err := ExecCommand(zerolog.InfoLevel, cnx, "/var/lib/uyuni-tools/"+scriptName)
	if err != nil {
		return fmt.Errorf("error running the migration script: %s", err)
	}
	return nil
}

// GenerateMigrationScript generates the script that perform migration.
func GenerateMigrationScript(sourceFqdn string, kubernetes bool) (string, error) {
	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	if err != nil {
		return "", fmt.Errorf("failed to create temporary directory: %s", err)
	}

	data := templates.MigrateScriptTemplateData{
		Volumes:    utils.ServerVolumeMounts,
		SourceFqdn: sourceFqdn,
		Kubernetes: kubernetes,
	}

	scriptPath := filepath.Join(scriptDir, "migrate.sh")
	if err = utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil {
		return "", fmt.Errorf("failed to generate migration script: %s", err)
	}

	return scriptDir, nil
}

// RunningImage returns the image running in the current system.
func RunningImage(cnx *shared.Connection, containerName string) (string, error) {
	command, err := cnx.GetCommand()

	switch command {
	case "podman":
		args := []string{"ps", "--format", "{{.Image}}", "--noheading"}
		image, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", args...)
		if err != nil {
			return "", err
		}
		return strings.Trim(string(image), "\n"), nil

	case "kubectl":

		//FIXME this will work until containers 0 is uyuni. Then jsonpath should be something like
		// {.items[0].spec.containers[?(@.name=="` + containerName + `")].image but there are problems
		// using RunCmdOutput with an arguments with round brackets
		args := []string{"get", "pods", kubernetes.ServerFilter, "-o", "jsonpath={.items[0].spec.containers[0].image}"}
		image, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", args...)

		log.Info().Msgf("image is: %s", image)
		if err != nil {
			return "", err
		}
		return strings.Trim(string(image), "\n"), nil
	}

	return command, err
}

// ReadInspectData returns a map with the values inspected by an image and deploy.
func ReadInspectData(scriptDir string, prefix ...string) (map[string]string, error) {
	path := filepath.Join(scriptDir, "data")
	log.Debug().Msgf("Trying to read %s", path)
	data, err := os.ReadFile(path)
	if err != nil {
		return map[string]string{}, fmt.Errorf("cannot parse file %s: %s", path, err)
	}

	inspectResult := make(map[string]string)

	viper.SetConfigType("env")
	if err := viper.ReadConfig(bytes.NewBuffer(data)); err != nil {
		return map[string]string{}, fmt.Errorf("cannot read config: %s", err)
	}

	for _, v := range inspectValues {
		if len(viper.GetString(v.Variable)) > 0 {
			index := v.Variable
			/* Just the first value of prefix is used.
			 * This slice is just to allow an empty argument
			 */
			if len(prefix) >= 1 {
				index = prefix[0] + v.Variable
			}
			inspectResult[index] = viper.GetString(v.Variable)
		}
	}
	return inspectResult, nil
}

// InspectHost check values on a host machine.
func InspectHost() (map[string]string, error) {
	scriptDir, err := os.MkdirTemp("", "mgradm-*")
	defer os.RemoveAll(scriptDir)
	if err != nil {
		return map[string]string{}, fmt.Errorf("failed to create temporary directory %s", err)
	}

	if err := GenerateInspectHostScript(scriptDir); err != nil {
		return map[string]string{}, err
	}

	if err := utils.RunCmdStdMapping(zerolog.DebugLevel, scriptDir+"/inspect.sh"); err != nil {
		return map[string]string{}, fmt.Errorf("failed to run inspect script in host system: %s", err)
	}

	inspectResult, err := ReadInspectData(scriptDir, "host_")
	if err != nil {
		return map[string]string{}, fmt.Errorf("cannot inspect host data. %s", err)
	}

	return inspectResult, err
}

// GenerateInspectContainerScript create the host inspect script.
func GenerateInspectHostScript(scriptDir string) error {
	data := templates.InspectTemplateData{
		Param:      inspectValues,
		OutputFile: scriptDir + "/" + InspectOutputFile.Basename,
	}

	scriptPath := filepath.Join(scriptDir, InspectScriptFilename)
	if err := utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil {
		return fmt.Errorf("failed to generate inspect script: %s", err)
	}
	return nil
}

// GenerateInspectContainerScript create the container inspect script.
func GenerateInspectContainerScript(scriptDir string) error {
	data := templates.InspectTemplateData{
		Param:      inspectValues,
		OutputFile: InspectOutputFile.Directory + "/" + InspectOutputFile.Basename,
	}

	scriptPath := filepath.Join(scriptDir, InspectScriptFilename)
	if err := utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil {
		return fmt.Errorf("failed to generate inspect script: %s", err)
	}
	return nil
}
0707010000009C000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001300000000uyuni-tools/mgrctl0707010000009D000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001700000000uyuni-tools/mgrctl/cmd0707010000009E000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001B00000000uyuni-tools/mgrctl/cmd/api0707010000009F000081A4000003E80000006400000001660688CE0000066E000000000000000000000000000000000000002200000000uyuni-tools/mgrctl/cmd/api/api.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package api

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type apiFlags struct {
	api.ConnectionDetails `mapstructure:"api"`
}

// NewCommand generates a JSON over HTTP API helper tool command.
func NewCommand(globalFlags *types.GlobalFlags) (*cobra.Command, error) {
	var flags apiFlags

	apiCmd := &cobra.Command{
		Use:   "api",
		Short: "JSON over HTTP API helper tool",
	}

	apiGet := &cobra.Command{
		Use:   "get path [parameters]...",
		Short: "Call API GET request",
		Long:  "Takes an API path and optional parameters and then issues GET request with the specified path and parameters. If user and password are provided, calls login before API call",
		RunE: func(cmd *cobra.Command, args []string) error {
			return utils.CommandHelper(globalFlags, cmd, args, &flags, runGet)
		},
	}

	apiPost := &cobra.Command{
		Use:   "post path parameters...",
		Short: "Call API POST request",
		Long:  "Takes an API path and parameters and then issues POST request with the specified path and parameters. User and password are mandatory. Parameters can be either JSON encoded string or one or more key=value pairs.",
		RunE: func(cmd *cobra.Command, args []string) error {
			return utils.CommandHelper(globalFlags, cmd, args, &flags, runPost)
		},
	}

	apiCmd.AddCommand(apiGet)
	apiCmd.AddCommand(apiPost)

	if err := api.AddAPIFlags(apiCmd, false); err != nil {
		return apiCmd, err
	}
	return apiCmd, nil
}
070701000000A0000081A4000003E80000006400000001660688CE000001A2000000000000000000000000000000000000002700000000uyuni-tools/mgrctl/cmd/api/api_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package api

import (
	"testing"

	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func TestNewCommand(t *testing.T) {
	var globalflags types.GlobalFlags
	cmd, err := NewCommand(&globalflags)
	if err != nil {
		t.Errorf("Unexpected error creating command: %s", err)
	}
	if cmd == nil {
		t.Error("Unexpected nil command")
	}
}
070701000000A1000081A4000003E80000006400000001660688CE00000426000000000000000000000000000000000000002200000000uyuni-tools/mgrctl/cmd/api/get.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package api

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"

	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func runGet(globalFlags *types.GlobalFlags, flags *apiFlags, cmd *cobra.Command, args []string) error {
	log.Debug().Msgf("Running GET command %s", args[0])
	client, err := api.Init(&flags.ConnectionDetails)

	if err != nil {
		return fmt.Errorf("unable to login to the server: %s", err)
	}
	path := args[0]
	options := args[1:]

	res, err := api.Get[interface{}](client, fmt.Sprintf("%s?%s", path, strings.Join(options, "&")))
	if err != nil {
		return fmt.Errorf("error in query %s: %s", path, err)
	}

	// TODO do this only when result is JSON or TEXT. Watchout for binary data
	// Decode JSON to the string and pretty print it
	out, err := json.MarshalIndent(res.Result, "", "  ")
	if err != nil {
		return err
	}
	fmt.Print(string(out))

	return nil
}
070701000000A2000081A4000003E80000006400000001660688CE00000573000000000000000000000000000000000000002300000000uyuni-tools/mgrctl/cmd/api/post.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package api

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"

	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func runPost(globalFlags *types.GlobalFlags, flags *apiFlags, cmd *cobra.Command, args []string) error {
	log.Debug().Msgf("Running POST command %s", args[0])
	client, err := api.Init(&flags.ConnectionDetails)

	if err != nil {
		log.Fatal().Err(err).Msg("Unable to login to the server")
	}

	path := args[0]
	options := args[1:]

	var data map[string]interface{}

	if len(options) > 1 {
		log.Debug().Msg("Multiple options specified, assuming non JSON data")
		data = map[string]interface{}{}
		for _, o := range options {
			s := strings.SplitN(o, "=", 2)
			data[s[0]] = s[1]
		}
	} else {
		if err := json.NewDecoder(strings.NewReader(args[1])).Decode(&data); err != nil {
			log.Debug().Msg("Failed to decode parameters as JSON, assuming key=value pairs")
		}
	}

	res, err := api.Post[interface{}](client, path, data)
	if err != nil {
		return fmt.Errorf("error in query %s: %s", path, err)
	}

	if !res.Success {
		log.Error().Msg(res.Message)
	}
	out, err := json.MarshalIndent(res.Result, "", "  ")
	if err != nil {
		log.Fatal().Err(err)
	}
	fmt.Print(string(out))

	return nil
}
070701000000A3000081A4000003E80000006400000001660688CE000009D4000000000000000000000000000000000000001E00000000uyuni-tools/mgrctl/cmd/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
	"os"
	"path"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/api"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/cp"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/exec"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/org"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/term"
	"github.com/uyuni-project/uyuni-tools/shared/completion"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// NewCommand returns a new cobra.Command implementing the root command for kinder.
func NewUyunictlCommand() (*cobra.Command, error) {
	globalFlags := &types.GlobalFlags{}
	name := path.Base(os.Args[0])
	rootCmd := &cobra.Command{
		Use:          name,
		Short:        L("Uyuni control tool"),
		Long:         L("Uyuni control tool used to help user managing Uyuni and SUSE Manager Servers mainly through its API"),
		Version:      utils.Version,
		SilenceUsage: true, // Don't show usage help on errors
	}

	usage, err := utils.GetUsageWithConfigHelpTemplate(rootCmd.UsageTemplate())
	if err != nil {
		return rootCmd, err
	}
	rootCmd.SetUsageTemplate(usage)

	rootCmd.PersistentFlags().StringVarP(&globalFlags.ConfigPath, "config", "c", "", "configuration file path")
	rootCmd.PersistentFlags().StringVar(&globalFlags.LogLevel, "logLevel", "", "application log level (trace|debug|info|warn|error|fatal|panic)")

	rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
		utils.LogInit(cmd.Name() != "exec" && cmd.Name() != "term")
		utils.SetLogLevel(globalFlags.LogLevel)

		// do not log if running the completion cmd as the output is redirect to create a file to source
		if cmd.Name() != "completion" {
			log.Info().Msgf("Welcome to %s", name)
			log.Info().Msgf("Executing command: %s", cmd.Name())
		}
	}

	apiCmd, err := api.NewCommand(globalFlags)
	if err != nil {
		log.Err(err).Msg("Failed to create api command")
	}
	rootCmd.AddCommand(apiCmd)
	rootCmd.AddCommand(exec.NewCommand(globalFlags))
	rootCmd.AddCommand(term.NewCommand(globalFlags))
	rootCmd.AddCommand(cp.NewCommand(globalFlags))
	rootCmd.AddCommand(completion.NewCommand(globalFlags))
	orgCmd, err := org.NewCommand(globalFlags)
	if err != nil {
		log.Err(err).Msg("Failed to create org command")
	}
	rootCmd.AddCommand(orgCmd)

	return rootCmd, nil
}
070701000000A4000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001A00000000uyuni-tools/mgrctl/cmd/cp070701000000A5000081A4000003E80000006400000001660688CE0000070B000000000000000000000000000000000000002000000000uyuni-tools/mgrctl/cmd/cp/cp.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package cp

import (
	"fmt"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type flagpole struct {
	User    string
	Group   string
	Backend string
}

// NewCommand copy file to and from the containers.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	flags := &flagpole{}

	cpCmd := &cobra.Command{
		Use:   "cp [path/to/source.file] [path/to/destination.file]",
		Short: "copy files to and from the containers",
		Long: `Takes a source and destination parameters.
	One of them can be prefixed with 'server:' to indicate the path is within the server pod.`,
		Args: cobra.ExactArgs(2),
		RunE: func(cmd *cobra.Command, args []string) error {
			viper, err := utils.ReadConfig(globalFlags.ConfigPath, cmd)
			if err != nil {
				return err
			}
			if err := viper.Unmarshal(&flags); err != nil {
				log.Error().Err(err).Msgf("Failed to unmarshall configuration")
				return fmt.Errorf("failed to unmarshall configuration: %s", err)
			}
			return run(flags, cmd, args)
		},
	}

	cpCmd.Flags().String("user", "", "User or UID to set on the destination file")
	cpCmd.Flags().String("group", "susemanager", "Group or GID to set on the destination file")

	utils.AddBackendFlag(cpCmd)
	return cpCmd
}

func run(flags *flagpole, cmd *cobra.Command, args []string) error {
	cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter)
	return cnx.Copy(args[0], args[1], flags.User, flags.Group)
}
070701000000A6000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001C00000000uyuni-tools/mgrctl/cmd/exec070701000000A7000081A4000003E80000006400000001660688CE00000F4C000000000000000000000000000000000000002400000000uyuni-tools/mgrctl/cmd/exec/exec.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package exec

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type flagpole struct {
	Envs        []string `mapstructure:"env"`
	Interactive bool
	Tty         bool
	Backend     string
}

// NewCommand returns a new cobra.Command for exec.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	var flags flagpole

	execCmd := &cobra.Command{
		Use:   "exec '[command-to-run --with-args]'",
		Short: "Execute commands inside the uyuni containers using 'sh -c'",
		RunE: func(cmd *cobra.Command, args []string) error {
			return utils.CommandHelper(globalFlags, cmd, args, &flags, run)
		},
	}

	execCmd.SetUsageTemplate(execCmd.UsageTemplate())

	execCmd.Flags().StringSliceP("env", "e", []string{}, "environment variables to pass to the command, separated by commas")
	execCmd.Flags().BoolP("interactive", "i", false, "Pass stdin to the container")
	execCmd.Flags().BoolP("tty", "t", false, "Stdin is a TTY")

	utils.AddBackendFlag(execCmd)
	return execCmd
}

func run(globalFlags *types.GlobalFlags, flags *flagpole, cmd *cobra.Command, args []string) error {
	cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter)
	podName, err := cnx.GetPodName()
	if err != nil {
		log.Fatal().Err(err)
	}

	command, err := cnx.GetCommand()
	if err != nil {
		log.Fatal().Err(err)
	}

	commandArgs := []string{"exec"}
	envs := []string{}
	envs = append(envs, flags.Envs...)
	if flags.Interactive {
		commandArgs = append(commandArgs, "-i")
		envs = append(envs, "ENV=/etc/sh.shrc.local")
	}
	if flags.Tty {
		commandArgs = append(commandArgs, "-t")
		envs = append(envs, "TERM")
	}
	commandArgs = append(commandArgs, podName)

	if command == "kubectl" {
		commandArgs = append(commandArgs, "-c", "uyuni", "--")
	}

	newEnv := []string{}
	for _, envValue := range envs {
		if !strings.Contains(envValue, "=") {
			if value, set := os.LookupEnv(envValue); set {
				newEnv = append(newEnv, fmt.Sprintf("%s=%s", envValue, value))
			}
		} else {
			newEnv = append(newEnv, envValue)
		}
	}
	if len(newEnv) > 0 {
		commandArgs = append(commandArgs, "env")
		commandArgs = append(commandArgs, newEnv...)
	}
	commandArgs = append(commandArgs, "sh", "-c", strings.Join(args, " "))
	err = RunRawCmd(command, commandArgs)
	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			log.Info().Err(err).Msg("Command failed")
			os.Exit(exitErr.ExitCode())
		}
	}
	log.Info().Msg("Command returned with exit code 0")

	return nil
}

type copyWriter struct {
	Stream io.Writer
}

// Write writes an array of buffer in a stream.
func (l copyWriter) Write(p []byte) (n int, err error) {
	// Filter out kubectl line about terminated exit code
	if !strings.HasPrefix(string(p), "command terminated with exit code") {
		if _, err := l.Stream.Write(p); err != nil {
			return 0, fmt.Errorf("cannot write: %s", err)
		}

		n = len(p)
		if n > 0 && p[n-1] == '\n' {
			// Trim CR added by stdlog.
			p = p[0 : n-1]
		}
		log.Debug().Msg(string(p))
	}
	return
}

// RunRawCmd runs a command, mapping stdout and start error, waiting and checking return code.
func RunRawCmd(command string, args []string) error {
	log.Info().Msgf("Running: %s %s", command, strings.Join(args, " "))

	runCmd := exec.Command(command, args...)
	runCmd.Stdin = os.Stdin

	runCmd.Stdout = copyWriter{Stream: os.Stdout}
	runCmd.Stderr = copyWriter{Stream: os.Stderr}

	if err := runCmd.Start(); err != nil {
		log.Debug().Err(err).Msg("error starting command")
		return err
	}

	return runCmd.Wait()
}
070701000000A8000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001B00000000uyuni-tools/mgrctl/cmd/org070701000000A9000081A4000003E80000006400000001660688CE000006F4000000000000000000000000000000000000002A00000000uyuni-tools/mgrctl/cmd/org/createFirst.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package org

import (
	"fmt"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/api/org"
	apiTypes "github.com/uyuni-project/uyuni-tools/shared/api/types"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type createFirstFlags struct {
	api.ConnectionDetails `mapstructure:"api"`
	Organization          string
	Admin                 apiTypes.User
}

func createFirstCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "createFirst",
		Short: "create the first user and organization",
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags createFirstFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, createFirst)
		},
	}

	cmd.Flags().String("admin-login", "admin", "Administrator user name")
	cmd.Flags().String("admin-password", "", "Administrator password. If empty, the first user will not be created")
	cmd.Flags().String("admin-firstName", "Administrator", "The first name of the administrator")
	cmd.Flags().String("admin-lastName", "McAdmin", "The last name of the administrator")
	cmd.Flags().String("admin-email", "root@localhost", "The administrator's email")
	cmd.Flags().String("organization", "Organiszation", "The first organization name")

	return cmd
}

func createFirst(globalFlags *types.GlobalFlags, flags *createFirstFlags, cmd *cobra.Command, args []string) error {
	org, err := org.CreateFirst(&flags.ConnectionDetails, flags.Organization, &flags.Admin)
	if err != nil {
		return err
	}

	fmt.Printf("Organization %s created with id %d", org.Name, org.Id)

	return nil
}
070701000000AA000081A4000003E80000006400000001660688CE0000024F000000000000000000000000000000000000002200000000uyuni-tools/mgrctl/cmd/org/org.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package org

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// NewCommand  command for APIs.
func NewCommand(globalFlags *types.GlobalFlags) (*cobra.Command, error) {
	orgCmd := &cobra.Command{
		Use:   "org",
		Short: "Organization-related commands",
	}

	if err := api.AddAPIFlags(orgCmd, false); err != nil {
		return orgCmd, err
	}

	orgCmd.AddCommand(createFirstCommand(globalFlags))

	return orgCmd, nil
}
070701000000AB000081A4000003E80000006400000001660688CE000001A2000000000000000000000000000000000000002700000000uyuni-tools/mgrctl/cmd/org/org_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package org

import (
	"testing"

	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func TestNewCommand(t *testing.T) {
	var globalflags types.GlobalFlags
	cmd, err := NewCommand(&globalflags)
	if err != nil {
		t.Errorf("Unexpected error creating command: %s", err)
	}
	if cmd == nil {
		t.Error("Unexpected nil command")
	}
}
070701000000AC000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001C00000000uyuni-tools/mgrctl/cmd/term070701000000AD000081A4000003E80000006400000001660688CE000003D8000000000000000000000000000000000000002400000000uyuni-tools/mgrctl/cmd/term/term.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package term

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/exec"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

var newExecCmd = exec.NewCommand

// NewCommand returns a new cobra.Command for term.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "term",
		Short: "Run a terminal inside the server container",
		RunE: func(cmd *cobra.Command, args []string) error {
			execCmd := newExecCmd(globalFlags)
			execArgs := []string{"-i", "-t"}
			backend, err := cmd.Flags().GetString("backend")
			if err == nil {
				execArgs = append(execArgs, "--backend", backend)
			}
			if err := execCmd.Flags().Parse(execArgs); err != nil {
				return err
			}
			return execCmd.RunE(execCmd, []string{"bash"})
		},
	}

	utils.AddBackendFlag(cmd)
	return cmd
}
070701000000AE000081A4000003E80000006400000001660688CE0000050C000000000000000000000000000000000000002900000000uyuni-tools/mgrctl/cmd/term/term_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package term

import (
	"errors"
	"testing"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/exec"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// Ensure the term command properly delegates to the exec one.
func TestExecute(t *testing.T) {
	var globalFlags types.GlobalFlags

	newExecCmd = func(globalFlags *types.GlobalFlags) *cobra.Command {
		execCmd := exec.NewCommand(globalFlags)
		execCmd.RunE = func(cmd *cobra.Command, args []string) error {
			if interactive, err := cmd.Flags().GetBool("interactive"); err != nil || !interactive {
				t.Error("interactive flag not passed")
			}
			if tty, err := cmd.Flags().GetBool("tty"); err != nil || !tty {
				t.Error("tty flag not passed")
			}
			if backend, err := cmd.Flags().GetString("backend"); err != nil || backend != "mybackend" {
				t.Error("backend flag not passed")
			}
			return errors.New("some error")
		}
		return execCmd
	}

	cmd := NewCommand(&globalFlags)
	if err := cmd.Flags().Parse([]string{"--backend", "mybackend"}); err != nil {
		t.Errorf("failed to parse flags: %s", err)
	}
	if err := cmd.RunE(cmd, []string{}); err.Error() != "some error" {
		t.Errorf("Unexpected error returned")
	}
}
070701000000AF000081A4000003E80000006400000001660688CE00000215000000000000000000000000000000000000001B00000000uyuni-tools/mgrctl/main.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package main

import (
	"os"

	"github.com/chai2010/gettext-go"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// Run runs the `mgrctl` root command.
func Run() error {
	gettext.BindLocale(gettext.New("mgrctl", utils.LocaleRoot))
	run, err := cmd.NewUyunictlCommand()
	if err != nil {
		return err
	}
	return run.Execute()
}

func main() {
	if err := Run(); err != nil {
		os.Exit(1)
	}
}
070701000000B0000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001300000000uyuni-tools/mgrpxy070701000000B1000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001700000000uyuni-tools/mgrpxy/cmd070701000000B2000081A4000003E80000006400000001660688CE000009A5000000000000000000000000000000000000001E00000000uyuni-tools/mgrpxy/cmd/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
	"os"
	"path"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/install"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/restart"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/start"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/status"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/stop"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/uninstall"
	"github.com/uyuni-project/uyuni-tools/shared/completion"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// NewCommand returns a new cobra.Command implementing the root command for kinder.
func NewUyuniproxyCommand() (*cobra.Command, error) {
	globalFlags := &types.GlobalFlags{}
	name := path.Base(os.Args[0])
	rootCmd := &cobra.Command{
		Use:          name,
		Short:        "Uyuni proxy administration tool",
		Long:         "Uyuni tool used to help user administer uyuni proxies on kubernetes and podman",
		Version:      utils.Version,
		SilenceUsage: true, // Don't show usage help on errors
	}

	usage, err := utils.GetUsageWithConfigHelpTemplate(rootCmd.UsageTemplate())
	if err != nil {
		return rootCmd, err
	}
	rootCmd.SetUsageTemplate(usage)

	rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
		utils.LogInit(true)
		utils.SetLogLevel(globalFlags.LogLevel)

		// do not log if running the completion cmd as the output is redirected to create a file to source
		if cmd.Name() != "completion" {
			log.Info().Msgf("Welcome to %s", name)
			log.Info().Msgf("Executing command: %s", cmd.Name())
		}
	}

	rootCmd.PersistentFlags().StringVarP(&globalFlags.ConfigPath, "config", "c", "", "configuration file path")
	rootCmd.PersistentFlags().StringVar(&globalFlags.LogLevel, "logLevel", "", "application log level (trace|debug|info|warn|error|fatal|panic)")

	installCmd := install.NewCommand(globalFlags)
	rootCmd.AddCommand(installCmd)
	uninstallCmd, err := uninstall.NewCommand(globalFlags)
	if err != nil {
		return rootCmd, err
	}
	rootCmd.AddCommand(uninstallCmd)
	rootCmd.AddCommand(completion.NewCommand(globalFlags))
	rootCmd.AddCommand(status.NewCommand(globalFlags))
	rootCmd.AddCommand(start.NewCommand(globalFlags))
	rootCmd.AddCommand(stop.NewCommand(globalFlags))
	rootCmd.AddCommand(restart.NewCommand(globalFlags))

	return rootCmd, nil
}
070701000000B3000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001F00000000uyuni-tools/mgrpxy/cmd/install070701000000B4000081A4000003E80000006400000001660688CE000002D9000000000000000000000000000000000000002A00000000uyuni-tools/mgrpxy/cmd/install/install.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package install

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/install/kubernetes"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/install/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// NewCommand install a new proxy from scratch.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	installCmd := &cobra.Command{
		Use:   "install [fqdn]",
		Short: "install a new proxy from scratch",
		Long:  "Install a new proxy from scratch",
	}

	installCmd.AddCommand(podman.NewCommand(globalFlags))
	installCmd.AddCommand(kubernetes.NewCommand(globalFlags))

	return installCmd
}
070701000000B5000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002A00000000uyuni-tools/mgrpxy/cmd/install/kubernetes070701000000B6000081A4000003E80000006400000001660688CE00000574000000000000000000000000000000000000003800000000uyuni-tools/mgrpxy/cmd/install/kubernetes/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/shared/kubernetes"
	pxy_utils "github.com/uyuni-project/uyuni-tools/mgrpxy/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type kubernetesProxyInstallFlags struct {
	pxy_utils.ProxyInstallFlags `mapstructure:",squash"`
	Helm                        kubernetes.HelmFlags
}

// NewCommand install a new proxy on a running kubernetes cluster.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "kubernetes [path/to/config.tar.gz]",
		Short: "install a new proxy on a running kubernetes cluster",
		Long: `Install a new proxy on a running kubernetes cluster.

It only takes the path to the configuration tarball generated by the server
as parameter.

The install kubernetes command assumes kubectl is installed locally.

NOTE: for now installing on a remote kubernetes cluster is not supported!
`,
		Args: cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags kubernetesProxyInstallFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, installForKubernetes)
		},
	}

	pxy_utils.AddInstallFlags(cmd)

	kubernetes.AddHelmFlags(cmd)

	return cmd
}
070701000000B7000081A4000003E80000006400000001660688CE000007B1000000000000000000000000000000000000003300000000uyuni-tools/mgrpxy/cmd/install/kubernetes/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"fmt"
	"os"
	"os/exec"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/shared/utils"
	shared_kubernetes "github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	shared_utils "github.com/uyuni-project/uyuni-tools/shared/utils"
)

func installForKubernetes(globalFlags *types.GlobalFlags,
	flags *kubernetesProxyInstallFlags, cmd *cobra.Command, args []string,
) error {
	for _, binary := range []string{"kubectl", "helm"} {
		if _, err := exec.LookPath(binary); err != nil {
			return fmt.Errorf("install %s before running this command", binary)
		}
	}

	// Unpack the tarball
	configPath := utils.GetConfigPath(args)

	tmpDir, err := os.MkdirTemp("", "mgrpxy-*")
	if err != nil {
		return fmt.Errorf("failed to create temporary directory")
	}
	defer os.RemoveAll(tmpDir)

	if err := shared_utils.ExtractTarGz(configPath, tmpDir); err != nil {
		return fmt.Errorf("failed to extract configuration")
	}

	// Check the kubernetes cluster setup
	clusterInfos, err := shared_kubernetes.CheckCluster()
	if err != nil {
		return err
	}

	// If installing on k3s, install the traefik helm config in manifests
	isK3s := clusterInfos.IsK3s()
	IsRke2 := clusterInfos.IsRke2()
	if isK3s {
		shared_kubernetes.InstallK3sTraefikConfig(shared_utils.PROXY_TCP_PORTS, shared_utils.UDP_PORTS)
	} else if IsRke2 {
		shared_kubernetes.InstallRke2NginxConfig(shared_utils.PROXY_TCP_PORTS, shared_utils.UDP_PORTS,
			flags.Helm.Proxy.Namespace)
	}

	// Install the uyuni proxy helm chart
	if err := kubernetes.Deploy(&flags.ProxyInstallFlags, &flags.Helm, tmpDir, clusterInfos.GetKubeconfig(),
		"--set", "ingress="+clusterInfos.Ingress); err != nil {
		return fmt.Errorf("cannot deploy proxy helm chart: %s", err)
	}

	return nil
}
070701000000B8000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002600000000uyuni-tools/mgrpxy/cmd/install/podman070701000000B9000081A4000003E80000006400000001660688CE0000053F000000000000000000000000000000000000003000000000uyuni-tools/mgrpxy/cmd/install/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	shared_utils "github.com/uyuni-project/uyuni-tools/shared/utils"
)

type podmanProxyInstallFlags struct {
	utils.ProxyInstallFlags `mapstructure:",squash"`
	Podman                  podman.PodmanFlags
}

// NewCommand install a new proxy on podman from scratch.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	podmanCmd := &cobra.Command{
		Use:   "podman [path/to/config.tar.gz]",
		Short: "install a new proxy on podman from scratch",
		Long: `Install a new proxy on podman from scratch

It only takes the path to the configuration tarball generated by the server
as parameter.

The install podman command assumes podman is installed locally.

NOTE: for now installing on a remote podman is not supported!
`,
		Args: cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags podmanProxyInstallFlags
			return shared_utils.CommandHelper(globalFlags, cmd, args, &flags, installForPodman)
		},
	}

	utils.AddInstallFlags(podmanCmd)
	podman.AddPodmanInstallFlag(podmanCmd)

	return podmanCmd
}
070701000000BA000081A4000003E80000006400000001660688CE00000C12000000000000000000000000000000000000002F00000000uyuni-tools/mgrpxy/cmd/install/podman/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"fmt"
	"os"
	"os/exec"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/shared/podman"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/shared/utils"
	shared_podman "github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	shared_utils "github.com/uyuni-project/uyuni-tools/shared/utils"
)

// Start the proxy services.
func startPod() error {
	ret := shared_podman.IsServiceRunning(shared_podman.ProxyService)
	if ret {
		return shared_podman.RestartService(shared_podman.ProxyService)
	} else {
		return shared_podman.EnableService(shared_podman.ProxyService)
	}
}

func installForPodman(globalFlags *types.GlobalFlags, flags *podmanProxyInstallFlags, cmd *cobra.Command, args []string) error {
	if _, err := exec.LookPath("podman"); err != nil {
		return fmt.Errorf("install podman before running this command")
	}

	configPath := utils.GetConfigPath(args)
	if err := unpackConfig(configPath); err != nil {
		return fmt.Errorf("failed to extract proxy config from %s file: %s", configPath, err)
	}

	httpdImage, err := getContainerImage(flags, "httpd")
	if err != nil {
		return err
	}
	saltBrokerImage, err := getContainerImage(flags, "salt-broker")
	if err != nil {
		return err
	}
	squidImage, err := getContainerImage(flags, "squid")
	if err != nil {
		return err
	}
	sshImage, err := getContainerImage(flags, "ssh")
	if err != nil {
		return err
	}
	tftpdImage, err := getContainerImage(flags, "tftpd")
	if err != nil {
		return err
	}

	// Setup the systemd service configuration options
	if err := podman.GenerateSystemdService(httpdImage, saltBrokerImage, squidImage, sshImage, tftpdImage, flags.Podman.Args); err != nil {
		return fmt.Errorf("cannot generate systemd file: %s", err)
	}

	return startPod()
}

func getContainerImage(flags *podmanProxyInstallFlags, name string) (string, error) {
	image := flags.GetContainerImage(name)
	inspectedHostValues, err := adm_utils.InspectHost()
	if err != nil {
		return "", fmt.Errorf("cannot inspect host values: %s", err)
	}

	pullArgs := []string{}
	_, scc_user_exist := inspectedHostValues["host_scc_username"]
	_, scc_user_password := inspectedHostValues["host_scc_password"]
	if scc_user_exist && scc_user_password {
		pullArgs = append(pullArgs, "--creds", inspectedHostValues["host_scc_username"]+":"+inspectedHostValues["host_scc_password"])
	}

	preparedImage, err := shared_podman.PrepareImage(image, flags.PullPolicy, pullArgs...)
	if err != nil {
		return "", err
	}

	return preparedImage, nil
}

func unpackConfig(configPath string) error {
	log.Info().Msgf("Setting up proxy with configuration %s", configPath)
	const proxyConfigDir = "/etc/uyuni/proxy"
	if err := os.MkdirAll(proxyConfigDir, 0755); err != nil {
		return err
	}

	if err := shared_utils.ExtractTarGz(configPath, proxyConfigDir); err != nil {
		return err
	}
	return nil
}
070701000000BB000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001F00000000uyuni-tools/mgrpxy/cmd/restart070701000000BC000081A4000003E80000006400000001660688CE000001AD000000000000000000000000000000000000002D00000000uyuni-tools/mgrpxy/cmd/restart/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package restart

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesRestart(
	globalFlags *types.GlobalFlags,
	flags *restartFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return kubernetes.Restart(kubernetes.ProxyFilter)
}
070701000000BD000081A4000003E80000006400000001660688CE000001A5000000000000000000000000000000000000002900000000uyuni-tools/mgrpxy/cmd/restart/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package restart

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func podmanRestart(
	globalFlags *types.GlobalFlags,
	flags *restartFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return podman.RestartService(podman.ProxyService)
}
070701000000BE000081A4000003E80000006400000001660688CE0000046F000000000000000000000000000000000000002A00000000uyuni-tools/mgrpxy/cmd/restart/restart.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package restart

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type restartFlags struct {
	Backend string
}

// NewCommand to restart server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	restartCmd := &cobra.Command{
		Use:   "restart",
		Short: "restart the proxy",
		Long:  "Restart the proxy",
		Args:  cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags restartFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, restart)
		},
	}
	restartCmd.SetUsageTemplate(restartCmd.UsageTemplate())

	utils.AddBackendFlag(restartCmd)

	return restartCmd
}

func restart(globalFlags *types.GlobalFlags, flags *restartFlags, cmd *cobra.Command, args []string) error {
	fn, err := shared.ChooseProxyPodmanOrKubernetes(cmd.Flags(), podmanRestart, kubernetesRestart)
	if err != nil {
		return err
	}

	return fn(globalFlags, flags, cmd, args)
}
070701000000BF000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001D00000000uyuni-tools/mgrpxy/cmd/start070701000000C0000081A4000003E80000006400000001660688CE000001A5000000000000000000000000000000000000002B00000000uyuni-tools/mgrpxy/cmd/start/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package start

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesStart(
	globalFlags *types.GlobalFlags,
	flags *startFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return kubernetes.Start(kubernetes.ProxyFilter)
}
070701000000C1000081A4000003E80000006400000001660688CE0000019D000000000000000000000000000000000000002700000000uyuni-tools/mgrpxy/cmd/start/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package start

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func podmanStart(
	globalFlags *types.GlobalFlags,
	flags *startFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return podman.StartService(podman.ProxyService)
}
070701000000C2000081A4000003E80000006400000001660688CE0000046F000000000000000000000000000000000000002600000000uyuni-tools/mgrpxy/cmd/start/start.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package start

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type startFlags struct {
	Backend string
}

// NewCommand starts the server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	startCmd := &cobra.Command{
		Use:   "start",
		Short: "start the proxy",
		Long:  "Start the proxy",
		Args:  cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags startFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, start)
		},
	}
	startCmd.SetUsageTemplate(startCmd.UsageTemplate())

	if utils.KubernetesBuilt {
		utils.AddBackendFlag(startCmd)
	}

	return startCmd
}

func start(globalFlags *types.GlobalFlags, flags *startFlags, cmd *cobra.Command, args []string) error {
	fn, err := shared.ChooseProxyPodmanOrKubernetes(cmd.Flags(), podmanStart, kubernetesStart)
	if err != nil {
		return err
	}

	return fn(globalFlags, flags, cmd, args)
}
070701000000C3000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001E00000000uyuni-tools/mgrpxy/cmd/status070701000000C4000081A4000003E80000006400000001660688CE000005B8000000000000000000000000000000000000002C00000000uyuni-tools/mgrpxy/cmd/status/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package status

import (
	"errors"
	"fmt"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesStatus(
	globalFlags *types.GlobalFlags,
	flags *statusFlags,
	cmd *cobra.Command,
	args []string,
) error {
	// Do we have an uyuni helm release?
	clusterInfos, err := kubernetes.CheckCluster()
	if err != nil {
		return fmt.Errorf("failed to discover the cluster type: %s", err)
	}

	kubeconfig := clusterInfos.GetKubeconfig()
	if !kubernetes.HasHelmRelease("uyuni-proxy", kubeconfig) {
		return errors.New("no uyuni-proxy helm release installed on the cluster")
	}

	namespace, err := kubernetes.FindNamespace("uyuni-proxy", kubeconfig)
	if err != nil {
		return fmt.Errorf("failed to find the uyuni-proxy deployment namespace: %s", err)
	}

	// Is the pod running? Do we have all the replicas?
	status, err := kubernetes.GetDeploymentStatus(namespace, "uyuni-proxy")
	if err != nil {
		return fmt.Errorf("failed to get deployment status: %s", err)
	}
	if status.Replicas != status.ReadyReplicas {
		log.Warn().Msgf("Some replicas are not ready: %d / %d", status.ReadyReplicas, status.Replicas)
	}

	if status.AvailableReplicas == 0 {
		return errors.New("the pod is not running")
	}

	log.Info().Msg("Proxy containers up and running")

	return nil
}
070701000000C5000081A4000003E80000006400000001660688CE00000399000000000000000000000000000000000000002800000000uyuni-tools/mgrpxy/cmd/status/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package status

import (
	"errors"
	"fmt"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func podmanStatus(
	globalFlags *types.GlobalFlags,
	flags *statusFlags,
	cmd *cobra.Command,
	args []string,
) error {
	var returnErr error
	services := []string{"httpd", "salt-broker", "squid", "ssh", "tftpd", "pod"}
	for _, service := range services {
		serviceName := fmt.Sprintf("uyuni-proxy-%s", service)
		if err := utils.RunCmdStdMapping(zerolog.DebugLevel, "systemctl", "status", serviceName); err != nil {
			log.Error().Err(err).Msgf("Failed to get status of the %s service", serviceName)
			returnErr = errors.New("failed to get the status of at least one service")
		}
	}
	return returnErr
}
070701000000C6000081A4000003E80000006400000001660688CE000004A8000000000000000000000000000000000000002800000000uyuni-tools/mgrpxy/cmd/status/status.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package status

import (
	"errors"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type statusFlags struct {
}

// NewCommand to get the status of the server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "status",
		Short: "get the proxy status",
		Long:  "Get the proxy status",
		Args:  cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags statusFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, status)
		},
	}
	cmd.SetUsageTemplate(cmd.UsageTemplate())

	return cmd
}

func status(globalFlags *types.GlobalFlags, flags *statusFlags, cmd *cobra.Command, args []string) error {
	if podman.HasService(podman.ProxyService) {
		return podmanStatus(globalFlags, flags, cmd, args)
	}

	if utils.IsInstalled("kubectl") && utils.IsInstalled("helm") {
		return kubernetesStatus(globalFlags, flags, cmd, args)
	}

	return errors.New("no installed proxy detected")
}
070701000000C7000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001C00000000uyuni-tools/mgrpxy/cmd/stop070701000000C8000081A4000003E80000006400000001660688CE000001A1000000000000000000000000000000000000002A00000000uyuni-tools/mgrpxy/cmd/stop/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package stop

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func kubernetesStop(
	globalFlags *types.GlobalFlags,
	flags *stopFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return kubernetes.Stop(kubernetes.ProxyFilter)
}
070701000000C9000081A4000003E80000006400000001660688CE00000199000000000000000000000000000000000000002600000000uyuni-tools/mgrpxy/cmd/stop/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package stop

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func podmanStop(
	globalFlags *types.GlobalFlags,
	flags *stopFlags,
	cmd *cobra.Command,
	args []string,
) error {
	return podman.StopService(podman.ProxyService)
}
070701000000CA000081A4000003E80000006400000001660688CE0000043D000000000000000000000000000000000000002400000000uyuni-tools/mgrpxy/cmd/stop/stop.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package stop

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type stopFlags struct {
	Backend string
}

// NewCommand to stop server.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	stopCmd := &cobra.Command{
		Use:   "stop",
		Short: "stop the proxy",
		Long:  "Stop the proxy",
		Args:  cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags stopFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, stop)
		},
	}

	stopCmd.SetUsageTemplate(stopCmd.UsageTemplate())

	utils.AddBackendFlag(stopCmd)

	return stopCmd
}

func stop(globalFlags *types.GlobalFlags, flags *stopFlags, cmd *cobra.Command, args []string) error {
	fn, err := shared.ChooseProxyPodmanOrKubernetes(cmd.Flags(), podmanStop, kubernetesStop)
	if err != nil {
		return err
	}

	return fn(globalFlags, flags, cmd, args)
}
070701000000CB000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002100000000uyuni-tools/mgrpxy/cmd/uninstall070701000000CC000081A4000003E80000006400000001660688CE00000445000000000000000000000000000000000000002F00000000uyuni-tools/mgrpxy/cmd/uninstall/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package uninstall

import (
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
)

func uninstallForKubernetes(dryRun bool) error {
	clusterInfos, err := kubernetes.CheckCluster()
	if err != nil {
		return err
	}
	kubeconfig := clusterInfos.GetKubeconfig()

	// TODO Find all the PVs related to the server if we want to delete them

	// Uninstall uyuni
	if _, err := kubernetes.HelmUninstall(kubeconfig, "uyuni-proxy", "", dryRun); err != nil {
		return err
	}

	// TODO Remove the PVs or wait for their automatic removal if purge is requested
	// Also wait if the PVs are dynamic with Delete reclaim policy but the user didn't ask to purge them
	// Since some storage plugins don't handle Delete policy, we may need to check for error events to avoid infinite loop

	// Remove the K3s Traefik config
	if clusterInfos.IsK3s() {
		kubernetes.UninstallK3sTraefikConfig(dryRun)
	}

	// Remove the rke2 nginx config
	if clusterInfos.IsRke2() {
		kubernetes.UninstallRke2NginxConfig(dryRun)
	}
	return nil
}
070701000000CD000081A4000003E80000006400000001660688CE000005D8000000000000000000000000000000000000002B00000000uyuni-tools/mgrpxy/cmd/uninstall/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package uninstall

import (
	"fmt"

	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func uninstallForPodman(dryRun bool, purge bool) error {
	// Uninstall the service
	podman.UninstallService("uyuni-proxy-pod", dryRun)
	podman.UninstallService("uyuni-proxy-httpd", dryRun)
	podman.UninstallService("uyuni-proxy-salt-broker", dryRun)
	podman.UninstallService("uyuni-proxy-squid", dryRun)
	podman.UninstallService("uyuni-proxy-ssh", dryRun)
	podman.UninstallService("uyuni-proxy-tftpd", dryRun)

	// Force stop the pod
	for _, containerName := range podman.ProxyContainerNames {
		podman.DeleteContainer(containerName, dryRun)
	}

	// Remove the volumes
	if purge {
		// Merge all proxy containers volumes into a map
		volumes := map[string]string{}
		allProxyVolumes := []map[string]string{
			utils.PROXY_HTTPD_VOLUMES,
			utils.PROXY_SQUID_VOLUMES,
			utils.PROXY_TFTPD_VOLUMES,
		}
		for _, volumesList := range allProxyVolumes {
			for volume, mount := range volumesList {
				volumes[volume] = mount
			}
		}

		// Delete each volume
		for volume := range volumes {
			if err := podman.DeleteVolume(volume, dryRun); err != nil {
				return fmt.Errorf("cannot delete volume %s: %s", volume, err)
			}
		}
		log.Info().Msg("All volumes removed")
	}

	podman.DeleteNetwork(dryRun)

	return podman.ReloadDaemon(dryRun)
}
070701000000CE000081A4000003E80000006400000001660688CE000006FA000000000000000000000000000000000000002E00000000uyuni-tools/mgrpxy/cmd/uninstall/uninstall.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package uninstall

import (
	"fmt"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// NewCommand for uninstall proxy.
func NewCommand(globalFlags *types.GlobalFlags) (*cobra.Command, error) {
	uninstallCmd := &cobra.Command{
		Use:   "uninstall",
		Short: "uninstall a proxy",
		Long: `Uninstall a proxy and optionally the corresponding volumes.
By default it will only print what would be done, use --force to actually remove.` + kubernetes.UninstallHelp,
		Args: cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			force, _ := cmd.Flags().GetBool("force")
			purge, _ := cmd.Flags().GetBool("purgeVolumes")

			backend, _ := cmd.Flags().GetString("backend")

			cnx := shared.NewConnection(backend, podman.ProxyContainerNames[0], kubernetes.ProxyFilter)
			command, err := cnx.GetCommand()
			if err != nil {
				log.Fatal().Err(err).Msg("Failed to determine suitable backend")
			}
			switch command {
			case "podman":
				if err := uninstallForPodman(!force, purge); err != nil {
					return fmt.Errorf("cannot uninstall podman: %s", err)
				}
			case "kubectl":
				if err := uninstallForKubernetes(!force); err != nil {
					return err
				}
			}
			return nil
		},
	}
	uninstallCmd.Flags().BoolP("force", "f", false, "Actually remove the server")
	uninstallCmd.Flags().Bool("purgeVolumes", false, "Also remove the volumes")

	utils.AddBackendFlag(uninstallCmd)

	return uninstallCmd, nil
}
070701000000CF000081A4000003E80000006400000001660688CE00000183000000000000000000000000000000000000001B00000000uyuni-tools/mgrpxy/main.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package main

import (
	"os"

	"github.com/uyuni-project/uyuni-tools/mgrpxy/cmd"
)

// Run runs the `mgrpxy` root command.
func Run() error {
	run, err := cmd.NewUyuniproxyCommand()
	if err != nil {
		return err
	}
	return run.Execute()
}

func main() {
	if err := Run(); err != nil {
		os.Exit(1)
	}
}
070701000000D0000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001A00000000uyuni-tools/mgrpxy/shared070701000000D1000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002500000000uyuni-tools/mgrpxy/shared/kubernetes070701000000D2000081A4000003E80000006400000001660688CE0000037D000000000000000000000000000000000000002C00000000uyuni-tools/mgrpxy/shared/kubernetes/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"fmt"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// HelmFlags it's used for helm chart flags.
type HelmFlags struct {
	Proxy types.ChartFlags
}

// AddHelmFlags add helm flags to a command.
func AddHelmFlags(cmd *cobra.Command) {
	defaultChart := fmt.Sprintf("oci://%s/proxy-helm", utils.DefaultNamespace)

	cmd.Flags().String("helm-proxy-namespace", "default", "Kubernetes namespace where to install the proxy")
	cmd.Flags().String("helm-proxy-chart", defaultChart, "URL to the proxy helm chart")
	cmd.Flags().String("helm-proxy-version", "", "Version of the proxy helm chart")
	cmd.Flags().String("helm-proxy-values", "", "Path to a values YAML file to use for proxy helm install")
}
070701000000D3000081A4000003E80000006400000001660688CE00000750000000000000000000000000000000000000002F00000000uyuni-tools/mgrpxy/shared/kubernetes/deploy.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"fmt"
	"path"

	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/shared/utils"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
)

const helmAppName = "uyuni-proxy"

// Deploy will deploy proxy in kubernetes.
func Deploy(installFlags *utils.ProxyInstallFlags, helmFlags *HelmFlags, configDir string,
	kubeconfig string, helmArgs ...string,
) error {
	log.Info().Msg("Installing Uyuni")

	helmParams := []string{}

	// Pass the user-provided values file
	extraValues := helmFlags.Proxy.Values
	if extraValues != "" {
		helmParams = append(helmParams, "-f", extraValues)
	}

	helmParams = append(helmParams,
		"-f", path.Join(configDir, "httpd.yaml"),
		"-f", path.Join(configDir, "ssh.yaml"),
		"-f", path.Join(configDir, "config.yaml"),
		"--set", "images.proxy-httpd="+installFlags.GetContainerImage("httpd"),
		"--set", "images.proxy-salt-broker="+installFlags.GetContainerImage("salt-broker"),
		"--set", "images.proxy-squid="+installFlags.GetContainerImage("squid"),
		"--set", "images.proxy-ssh="+installFlags.GetContainerImage("ssh"),
		"--set", "images.proxy-tftpd="+installFlags.GetContainerImage("tftpd"),
		"--set", "repository="+installFlags.ImagesLocation,
		"--set", "version="+installFlags.Tag,
		"--set", "pullPolicy="+kubernetes.GetPullPolicy(installFlags.PullPolicy))

	helmParams = append(helmParams, helmArgs...)

	// Install the helm chart
	if err := kubernetes.HelmUpgrade(kubeconfig, helmFlags.Proxy.Namespace, true, "", helmAppName, helmFlags.Proxy.Chart,
		helmFlags.Proxy.Version, helmParams...); err != nil {
		return fmt.Errorf("cannot run helm upgrade: %s", err)
	}

	// Wait for the pod to be started
	return kubernetes.WaitForDeployment(helmFlags.Proxy.Namespace, helmAppName, "uyuni-proxy")
}
070701000000D4000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002100000000uyuni-tools/mgrpxy/shared/podman070701000000D5000081A4000003E80000006400000001660688CE00000CFA000000000000000000000000000000000000002B00000000uyuni-tools/mgrpxy/shared/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"fmt"
	"path"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/mgrpxy/shared/templates"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// GenerateSystemdService generates all the systemd files required by proxy.
func GenerateSystemdService(httpdImage string, saltBrokerImage string, squidImage string, sshImage string,
	tftpdImage string, podmanArgs []string) error {
	if err := podman.SetupNetwork(); err != nil {
		return fmt.Errorf("cannot setup network: %s", err)
	}

	log.Info().Msg("Generating systemd services")
	httpProxyConfig := getHttpProxyConfig()

	ports := []types.PortMap{}
	ports = append(ports, utils.PROXY_TCP_PORTS...)
	ports = append(ports, utils.PROXY_PODMAN_PORTS...)
	ports = append(ports, utils.UDP_PORTS...)

	// Pod
	dataPod := templates.PodTemplateData{
		Ports:         ports,
		HttpProxyFile: httpProxyConfig,
		Args:          strings.Join(podmanArgs, " "),
	}
	if err := generateSystemdFile(dataPod, "pod"); err != nil {
		return fmt.Errorf("cannot generated systemd file: %s", err)
	}

	// Httpd
	dataHttpd := templates.HttpdTemplateData{
		Volumes:       utils.PROXY_HTTPD_VOLUMES,
		HttpProxyFile: httpProxyConfig,
		Image:         httpdImage,
	}
	if err := generateSystemdFile(dataHttpd, "httpd"); err != nil {
		return fmt.Errorf("cannot generated systemd file: %s", err)
	}

	// Salt broker
	dataSaltBroker := templates.SaltBrokerTemplateData{
		HttpProxyFile: httpProxyConfig,
		Image:         saltBrokerImage,
	}
	if err := generateSystemdFile(dataSaltBroker, "salt-broker"); err != nil {
		return fmt.Errorf("cannot generated systemd file: %s", err)
	}

	// Squid
	dataSquid := templates.SquidTemplateData{
		Volumes:       utils.PROXY_SQUID_VOLUMES,
		HttpProxyFile: httpProxyConfig,
		Image:         squidImage,
	}
	if err := generateSystemdFile(dataSquid, "squid"); err != nil {
		return fmt.Errorf("cannot generated systemd file: %s", err)
	}

	// SSH
	dataSSH := templates.SSHTemplateData{
		HttpProxyFile: httpProxyConfig,
		Image:         sshImage,
	}
	if err := generateSystemdFile(dataSSH, "ssh"); err != nil {
		return fmt.Errorf("cannot generated systemd file: %s", err)
	}

	// Tftpd
	dataTftpd := templates.TFTPDTemplateData{
		Volumes:       utils.PROXY_TFTPD_VOLUMES,
		HttpProxyFile: httpProxyConfig,
		Image:         tftpdImage,
	}
	if err := generateSystemdFile(dataTftpd, "tftpd"); err != nil {
		return fmt.Errorf("cannot generated systemd file: %s", err)
	}

	return podman.ReloadDaemon(false)
}

func generateSystemdFile(template utils.Template, service string) error {
	name := fmt.Sprintf("uyuni-proxy-%s.service", service)

	const systemdPath = "/etc/systemd/system"
	path := path.Join(systemdPath, name)
	if err := utils.WriteTemplateToFile(template, path, 0644, true); err != nil {
		return fmt.Errorf("failed to generate %s", path)
	}
	return nil
}

func getHttpProxyConfig() string {
	const httpProxyConfigPath = "/etc/sysconfig/proxy"

	// Only SUSE distros seem to have such a file for HTTP proxy settings
	if utils.FileExists(httpProxyConfigPath) {
		return httpProxyConfigPath
	}
	return ""
}
070701000000D6000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002400000000uyuni-tools/mgrpxy/shared/templates070701000000D7000081A4000003E80000006400000001660688CE000006CE000000000000000000000000000000000000002D00000000uyuni-tools/mgrpxy/shared/templates/httpd.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

const httpdTemplate = `# uyuni-proxy-httpd.service, generated by mgrpxy
# Use an uyuni-proxy-httpd.service.d/local.conf file to override

[Unit]
Description=Uyuni proxy httpd container service
Wants=network.target
After=network-online.target
BindsTo=uyuni-proxy-pod.service
After=uyuni-proxy-pod.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Environment=UYUNI_IMAGE={{ .Image }}
{{- if .HttpProxyFile }}
EnvironmentFile={{ .HttpProxyFile }}
{{- end }}
Restart=on-failure
ExecStartPre=/bin/rm -f %t/uyuni-proxy-httpd.pid %t/uyuni-proxy-httpd.ctr-id

ExecStart=/usr/bin/podman run \
	--conmon-pidfile %t/uyuni-proxy-httpd.pid \
	--cidfile %t/uyuni-proxy-httpd.ctr-id \
	--cgroups=no-conmon \
	--pod-id-file %t/uyuni-proxy-pod.pod-id -d \
	--replace -dt \
	-v /etc/uyuni/proxy:/etc/uyuni:ro \
	{{- range $name, $path := .Volumes }}
	-v {{ $name }}:{{ $path }} \
	{{- end }}
	--name uyuni-proxy-httpd \
	${UYUNI_IMAGE}

ExecStop=/usr/bin/podman stop --ignore --cidfile %t/uyuni-proxy-httpd.ctr-id -t 10
ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/uyuni-proxy-httpd.ctr-id
PIDFile=%t/uyuni-proxy-httpd.pid
TimeoutStopSec=60
Type=forking

[Install]
WantedBy=multi-user.target default.target
`

// HttpdTemplateData represents HTTPD information to create systemd file.
type HttpdTemplateData struct {
	Volumes       map[string]string
	HttpProxyFile string
	Image         string
}

// Render will create the systemd configuration file.
func (data HttpdTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("service").Parse(httpdTemplate))
	return t.Execute(wr, data)
}
070701000000D8000081A4000003E80000006400000001660688CE00000776000000000000000000000000000000000000002B00000000uyuni-tools/mgrpxy/shared/templates/pod.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"

	"github.com/uyuni-project/uyuni-tools/shared/types"
)

const podTemplate = `# uyuni-proxy-pod.service, generated by mgrpxy

[Unit]
Description=Podman uyuni-proxy-pod.service
Wants=network.target
After=network-online.target
Requires=uyuni-proxy-httpd.service uyuni-proxy-salt-broker.service uyuni-proxy-squid.service uyuni-proxy-ssh.service uyuni-proxy-tftpd.service
Before=uyuni-proxy-httpd.service uyuni-proxy-salt-broker.service uyuni-proxy-squid.service uyuni-proxy-ssh.service uyuni-proxy-tftpd.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
{{- if .HttpProxyFile }}
EnvironmentFile={{ .HttpProxyFile }}
{{- end }}
Restart=on-failure
ExecStartPre=/bin/rm -f %t/uyuni-proxy-pod.pid %t/uyuni-proxy-pod.pod-id

ExecStartPre=/usr/bin/podman pod create --infra-conmon-pidfile %t/uyuni-proxy-pod.pid \
		--pod-id-file %t/uyuni-proxy-pod.pod-id --name uyuni-proxy-pod \
        {{- range .Ports }}
        -p {{ .Exposed }}:{{ .Port }}{{ if .Protocol }}/{{ .Protocol }}{{ end }} \
        {{- end }}
		--replace {{ .Args }}

ExecStart=/usr/bin/podman pod start --pod-id-file %t/uyuni-proxy-pod.pod-id
ExecStop=/usr/bin/podman pod stop --ignore --pod-id-file %t/uyuni-proxy-pod.pod-id -t 10
ExecStopPost=/usr/bin/podman pod rm --ignore -f --pod-id-file %t/uyuni-proxy-pod.pod-id

PIDFile=%t/uyuni-proxy-pod.pid
TimeoutStopSec=60
Type=forking

[Install]
WantedBy=multi-user.target default.target
`

// PodTemplateData POD information to create systemd file.
type PodTemplateData struct {
	Ports         []types.PortMap
	HttpProxyFile string
	Args          string
}

// Render will create the systemd configuration file.
func (data PodTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("service").Parse(podTemplate))
	return t.Execute(wr, data)
}
070701000000D9000081A4000003E80000006400000001660688CE000006BD000000000000000000000000000000000000003300000000uyuni-tools/mgrpxy/shared/templates/salt-broker.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

const saltBrokerTemplate = `# uyuni-proxy-salt-broker.service, generated by mgrpxy
# Use an uyuni-proxy-salt-broker.service.d/local.conf file to override

[Unit]
Description=Uyuni proxy Salt broker container service
Wants=network.target
After=network-online.target
BindsTo=uyuni-proxy-pod.service
After=uyuni-proxy-pod.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Environment=UYUNI_IMAGE={{ .Image }}
{{- if .HttpProxyFile }}
EnvironmentFile={{ .HttpProxyFile }}
{{- end }}
Restart=on-failure
ExecStartPre=/bin/rm -f %t/uyuni-proxy-salt-broker.pid %t/uyuni-proxy-salt-broker.ctr-id

ExecStart=/usr/bin/podman run \
	--conmon-pidfile %t/uyuni-proxy-salt-broker.pid \
	--cidfile %t/uyuni-proxy-salt-broker.ctr-id \
	--cgroups=no-conmon \
	--pod-id-file %t/uyuni-proxy-pod.pod-id -d \
	--replace -dt \
	-v /etc/uyuni/proxy:/etc/uyuni:ro \
	--name uyuni-proxy-salt-broker \
	${UYUNI_IMAGE}

ExecStop=/usr/bin/podman stop --ignore --cidfile %t/uyuni-proxy-salt-broker.ctr-id -t 10
ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/uyuni-proxy-salt-broker.ctr-id
PIDFile=%t/uyuni-proxy-salt-broker.pid
TimeoutStopSec=60
Type=forking

[Install]
WantedBy=multi-user.target default.target
`

// SaltBrokerTemplateData represents Salt Broker information to create systemd file.
type SaltBrokerTemplateData struct {
	HttpProxyFile string
	Image         string
}

// Render will create the systemd configuration file.
func (data SaltBrokerTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("service").Parse(saltBrokerTemplate))
	return t.Execute(wr, data)
}
070701000000DA000081A4000003E80000006400000001660688CE000006C3000000000000000000000000000000000000002D00000000uyuni-tools/mgrpxy/shared/templates/squid.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

const squidTemplate = `# uyuni-proxy-squid.service, generated by mgrpxy
# Use an uyuni-proxy-squid.service.d/local.conf file to override

[Unit]
Description=Uyuni proxy squid container service
Wants=network.target
After=network-online.target
BindsTo=uyuni-proxy-pod.service
After=uyuni-proxy-pod.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Environment=UYUNI_IMAGE={{ .Image }}
{{- if .HttpProxyFile }}
EnvironmentFile={{ .HttpProxyFile }}
{{- end }}
Restart=on-failure
ExecStartPre=/bin/rm -f %t/uyuni-proxy-squid.pid %t/uyuni-proxy-squid.ctr-id

ExecStart=/usr/bin/podman run \
	--conmon-pidfile %t/uyuni-proxy-squid.pid \
	--cidfile %t/uyuni-proxy-squid.ctr-id \
	--cgroups=no-conmon \
	--pod-id-file %t/uyuni-proxy-pod.pod-id -d \
	--replace -dt \
	-v /etc/uyuni/proxy:/etc/uyuni:ro \
	{{- range $name, $path := .Volumes }}
	-v {{ $name }}:{{ $path }} \
	{{- end }}
	--name uyuni-proxy-squid \
	${UYUNI_IMAGE}

ExecStop=/usr/bin/podman stop --ignore --cidfile %t/uyuni-proxy-squid.ctr-id -t 10
ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/uyuni-proxy-squid.ctr-id
PIDFile=%t/uyuni-proxy-squid.pid
TimeoutStopSec=60
Type=forking

[Install]
WantedBy=multi-user.target default.target
`

// SquidTemplateData Squid information to create systemd file.
type SquidTemplateData struct {
	Volumes       map[string]string
	HttpProxyFile string
	Image         string
}

// Render will create the systemd configuration file.
func (data SquidTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("service").Parse(squidTemplate))
	return t.Execute(wr, data)
}
070701000000DB000081A4000003E80000006400000001660688CE0000062F000000000000000000000000000000000000002B00000000uyuni-tools/mgrpxy/shared/templates/ssh.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

const sshTemplate = `# uyuni-proxy-ssh.service, generated by mgrpxy
# Use an uyuni-proxy-ssh.service.d/local.conf file to override

[Unit]
Description=Uyuni proxy ssh container service
Wants=network.target
After=network-online.target
BindsTo=uyuni-proxy-pod.service
After=uyuni-proxy-pod.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Environment=UYUNI_IMAGE={{ .Image }}
{{- if .HttpProxyFile }}
EnvironmentFile={{ .HttpProxyFile }}
{{- end }}
Restart=on-failure
ExecStartPre=/bin/rm -f %t/uyuni-proxy-ssh.pid %t/uyuni-proxy-ssh.ctr-id

ExecStart=/usr/bin/podman run \
	--conmon-pidfile %t/uyuni-proxy-ssh.pid \
	--cidfile %t/uyuni-proxy-ssh.ctr-id \
	--cgroups=no-conmon \
	--pod-id-file %t/uyuni-proxy-pod.pod-id -d \
	--replace -dt \
	-v /etc/uyuni/proxy:/etc/uyuni:ro \
	--name uyuni-proxy-ssh \
	${UYUNI_IMAGE}

ExecStop=/usr/bin/podman stop --ignore --cidfile %t/uyuni-proxy-ssh.ctr-id -t 10
ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/uyuni-proxy-ssh.ctr-id
PIDFile=%t/uyuni-proxy-ssh.pid
TimeoutStopSec=60
Type=forking

[Install]
WantedBy=multi-user.target default.target
`

// SSHTemplateData SSH information to create systemd file.
type SSHTemplateData struct {
	HttpProxyFile string
	Image         string
}

// Render will create the systemd configuration file.
func (data SSHTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("service").Parse(sshTemplate))
	return t.Execute(wr, data)
}
070701000000DC000081A4000003E80000006400000001660688CE000006E8000000000000000000000000000000000000002D00000000uyuni-tools/mgrpxy/shared/templates/tftpd.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package templates

import (
	"io"
	"text/template"
)

const tftpdTemplate = `# uyuni-proxy-tftpd.service, generated by mgrpxy
# Use an uyuni-proxy-tftpd.service.d/local.conf file to override

[Unit]
Description=Uyuni proxy tftpd container service
Wants=network.target
After=network-online.target
BindsTo=uyuni-proxy-pod.service
After=uyuni-proxy-pod.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Environment=UYUNI_IMAGE={{ .Image }}
{{- if .HttpProxyFile }}
EnvironmentFile={{ .HttpProxyFile }}
{{- end }}
Restart=on-failure
ExecStartPre=/bin/rm -f %t/uyuni-proxy-tftpd.pid %t/uyuni-proxy-tftpd.ctr-id

ExecStart=/usr/bin/podman run \
	--conmon-pidfile %t/uyuni-proxy-tftpd.pid \
	--cidfile %t/uyuni-proxy-tftpd.ctr-id \
	--cgroups=no-conmon \
	--pod-id-file %t/uyuni-proxy-pod.pod-id -d \
	--replace -dt \
	-v /etc/uyuni/proxy:/etc/uyuni:ro \
	{{- range $name, $path := .Volumes }}
	 -v {{ $name }}:{{ $path }} \
	{{- end }}
	--name uyuni-proxy-tftpd \
	${UYUNI_IMAGE}

ExecStop=/usr/bin/podman stop --ignore --cidfile %t/uyuni-proxy-tftpd.ctr-id -t 10
ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/uyuni-proxy-tftpd.ctr-id
PIDFile=%t/uyuni-proxy-tftpd.pid
TimeoutStopSec=60
Type=forking

[Install]
WantedBy=multi-user.target default.target
`

// TFTPDTemplateData represents information used to create TFTPD systemd configuration file.
type TFTPDTemplateData struct {
	Volumes       map[string]string
	HttpProxyFile string
	Image         string
}

// Render will create the TFTPD systemd configuration file.
func (data TFTPDTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("service").Parse(tftpdTemplate))
	return t.Execute(wr, data)
}
070701000000DD000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000002000000000uyuni-tools/mgrpxy/shared/utils070701000000DE000081A4000003E80000006400000001660688CE000001C1000000000000000000000000000000000000002700000000uyuni-tools/mgrpxy/shared/utils/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// GetConfigPath returns the configuration path if exists.
func GetConfigPath(args []string) string {
	configPath := args[0]
	if !utils.FileExists(configPath) {
		log.Fatal().Msgf("argument is not an existing file: %s", configPath)
	}
	return configPath
}
070701000000DF000081A4000003E80000006400000001660688CE00000A1B000000000000000000000000000000000000002900000000uyuni-tools/mgrpxy/shared/utils/flags.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"fmt"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// ProxyInstallFlags are the flags used by install proxy command.
type ProxyInstallFlags struct {
	ImagesLocation string           `mapstructure:"imagesLocation"`
	Tag            string           `namespace:"tag"`
	PullPolicy     string           `mapstructure:"pullPolicy"`
	Httpd          types.ImageFlags `mapstructure:"httpd"`
	SaltBroker     types.ImageFlags `mapstructure:"saltBroker"`
	Squid          types.ImageFlags `mapstructure:"squid"`
	Ssh            types.ImageFlags `mapstructure:"ssh"`
	Tftpd          types.ImageFlags `mapstructure:"tftpd"`
}

// Get the full container image name and tag for a container name.
func (f *ProxyInstallFlags) GetContainerImage(name string) string {
	imageName := "proxy-" + name
	image := fmt.Sprintf("%s/%s", f.ImagesLocation, imageName)
	tag := f.Tag

	var containerImage *types.ImageFlags
	switch name {
	case "httpd":
		containerImage = &f.Httpd
	case "salt-broker":
		containerImage = &f.SaltBroker
	case "squid":
		containerImage = &f.Squid
	case "ssh":
		containerImage = &f.Ssh
	case "tftpd":
		containerImage = &f.Tftpd
	default:
		log.Warn().Msgf("Invalid proxy container name: %s", name)
	}

	if containerImage != nil {
		if containerImage.Name != "" {
			image = containerImage.Name
		}
		if containerImage.Tag != "" {
			tag = containerImage.Tag
		}
	}

	imageUrl, err := utils.ComputeImage(image, tag)
	if err != nil {
		log.Fatal().Err(err).Msg("Failed to compute image URL")
	}
	return imageUrl
}

// AddInstallFlags will add the proxy install flags to a command.
func AddInstallFlags(cmd *cobra.Command) {
	cmd.Flags().String("imagesLocation", utils.DefaultNamespace,
		"registry URL prefix containing the all the container images")
	cmd.Flags().String("tag", utils.DefaultTag, "Tag Image")
	utils.AddPullPolicyFlag(cmd)

	addContainerImageFlags(cmd, "httpd")
	addContainerImageFlags(cmd, "saltBroker")
	addContainerImageFlags(cmd, "squid")
	addContainerImageFlags(cmd, "ssh")
	addContainerImageFlags(cmd, "tftpd")
}

func addContainerImageFlags(cmd *cobra.Command, container string) {
	cmd.Flags().String(container+"-image", "",
		fmt.Sprintf("Image for %s container, overrides the namespace if set", container))
	cmd.Flags().String(container+"-tag", "",
		fmt.Sprintf("Tag for %s container, overrides the global value if set", container))
}
070701000000E0000081A4000003E80000006400000001660688CE00000AD7000000000000000000000000000000000000001700000000uyuni-tools/modules.md<!--
SPDX-FileCopyrightText: 2023 SUSE LLC

SPDX-License-Identifier: Apache-2.0
-->

The goal of this content is to set a high-level overview of each tool available.

For tools that depend on the backend we should explicitly specify which one we want to use. Backend can also be defined in the configuration file to be used by the user.

## Tools definition

In case one wants to add a new sub-command it should decide to in which tool it should be placed.

If the new sub-command needs access to the host OS of direct access to a running container then it should be added to MGRADM.

Commands in MGRCTL should use the API only. 

Any command to manage the proxy deployment must be placed in MGRPXY.

MGRDEV is focused on utility commands to be used during the development process.


## MGRADM

**Goals and definition:**

Install, update, and maintain a containerized Uyuni Server. Commands placed here will have/need access to the container runtime environment and also to the HOST OS.

Any new command that needs direct access to the host OS or any running container must be added to these tools.

**Target Stakeholder:** Uyuni administrator
**Where to install:** System where Uyuni Server should be deployed
**Sub-commands Naming:** verb -> backend

## MGRCTL

**Goals and definition:**
Helper tool for day-to-day operations and integration with other tools.
Sub-commands in this tool should use the API calls (although case-by-case exceptions can be considered if there are valid reasons).

**Target Stakeholder:** Uyuni operators
**Where to install:** System where the Uyuni Server is deployed, or in the operator machine (supporting the same Operating Systems we already support for `spacecmd`).
**Sub-commands Naming:** subcommand -> verb

## MGRDEV

**Goals and definition:**
Utility commands to be used during development process. This tool can have commands that run remotely on the host OS or on running containers. These commands can use SSH and podman-socket.
Examples of sub-commands are `cp` and `exec`.

**Target Stakeholder:** Uyuni Developers
**Where to install:** Any machine that needs remote access to running containers.
**Sub-commands Naming:** verb -> backend


## MGRPXY

**Goals and definition:**
Install and manage a containerized Uyuni Proxy. This new command is a proposal to solve the problem of managing the Proxy using the same tool that manages the server, and how that can lead to confusion and errors.

**Target Stakeholder:** Uyuni administrator
**Where to install:** System where the Uyuni Proxy should be deployed
**Sub-commands Naming:** verb -> backend

This command is to be developed in a later stage since it would be better to redefine how we deploy containerized proxy and follow the same approach we have provided in the server.
070701000000E1000081A4000003E80000006400000001660688CE000001E2000000000000000000000000000000000000001400000000uyuni-tools/push.sh#!/usr/bin/bash

# SPDX-FileCopyrightText: 2024 SUSE LLC
#
# SPDX-License-Identifier: Apache-2.0

# This script is called by push-packages-to-obs

OSCAPI=$1
GIT_DIR=$2
PKG_NAME=$3

SRPM_PKG_DIR=$(dirname "$0")

if [ "${OSCAPI}" == "https://api.suse.de" ]; then
  sed 's/^tag=%{!?_default_tag:latest}/tag=5.0.0-beta1/' -i ${SRPM_PKG_DIR}/uyuni-tools.spec
  sed "s/namespace='%{_default_namespace}'/namespace='%{_default_namespace}\/%{_arch}'/" -i ${SRPM_PKG_DIR}/uyuni-tools.spec
fi
070701000000E2000081ED000003E80000006400000001660688CE000000C6000000000000000000000000000000000000001500000000uyuni-tools/setup.sh# SPDX-FileCopyrightText: 2023 SUSE LLC
#
# SPDX-License-Identifier: Apache-2.0

set -euxo pipefail

go mod vendor && tar czvf vendor.tar.gz vendor >/dev/null && rm -rf vendor

echo "vendor.tar.gz"
070701000000E3000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001300000000uyuni-tools/shared070701000000E4000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001700000000uyuni-tools/shared/api070701000000E5000081A4000003E80000006400000001660688CE00001E2A000000000000000000000000000000000000001E00000000uyuni-tools/shared/api/api.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package api

import (
	"crypto/tls"
	"crypto/x509"
	"os"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/utils"

	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

const root_path_apiv1 = "/rhn/manager/api"

// HTTP Client is an API entrypoint.
type HTTPClient struct {

	// URL to the API endpoint of the target host
	BaseURL string

	// net/http client
	Client *http.Client

	// Authentication cookie storage
	AuthCookie *http.Cookie
}

// Connection details for initial API connection.
type ConnectionDetails struct {

	// FQDN of the target host.
	Server string

	// User to login under.
	User string

	// Password for the user.
	Password string

	// CA certificate used for target host validation.
	// Provided certificate is used together with system certificates.
	CAcert string

	// Disable certificate validation, unsecure and not recommended.
	Insecure bool
}

// API response where T is the type of the result.
type ApiResponse[T interface{}] struct {
	Result  T
	Success bool
	Message string
}

// AddAPIFlags is a helper to include api details for the provided command tree.
//
// If the api support is only optional for the command, set optional parameter to true.
func AddAPIFlags(cmd *cobra.Command, optional bool) error {
	cmd.PersistentFlags().String("api-server", "", "FQDN of the server to connect to")
	cmd.PersistentFlags().String("api-user", "", "API user username")
	cmd.PersistentFlags().String("api-password", "", "Password for the API user")
	cmd.PersistentFlags().String("api-cacert", "", "Path to a cert file of the CA")
	cmd.PersistentFlags().Bool("api-insecure", false, "If set, server certificate will not be checked for validity")

	if !optional {
		if err := cmd.MarkPersistentFlagRequired("api-server"); err != nil {
			return err
		}
		if err := cmd.MarkPersistentFlagRequired("api-user"); err != nil {
			return err
		}
		if err := cmd.MarkPersistentFlagRequired("api-password"); err != nil {
			return err
		}
	}
	return nil
}

func prettyPrint(v interface{}) string {
	b, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return ""
	}
	return fmt.Sprintln(string(b))
}

func (c *HTTPClient) sendRequest(req *http.Request) (*http.Response, error) {
	log.Debug().Msgf("Sending %s request %s", req.Method, req.URL)
	req.Header.Set("Content-Type", "application/json; charset=utf-8")
	req.Header.Set("Accept", "application/json; charset=utf-8")
	if c.AuthCookie != nil {
		req.AddCookie(c.AuthCookie)
	}

	log.Trace().Msg(prettyPrint(req.Header))
	log.Trace().Msg(prettyPrint(req.Body))

	res, err := c.Client.Do(req)
	if err != nil {
		log.Trace().Msgf("Request failed: %s", err)
		return nil, err
	}

	log.Trace().Msg(prettyPrint(res.Header))
	log.Trace().Msg(prettyPrint(res.Body))

	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
		var errResponse map[string]string
		if err = json.NewDecoder(res.Body).Decode(&errResponse); err == nil {
			return nil, fmt.Errorf(errResponse["message"])
		}
		return nil, fmt.Errorf("unknown error: %d", res.StatusCode)
	}
	log.Debug().Msgf("Received response with code %d", res.StatusCode)

	return res, nil
}

// Init returns a HTTPClient object for further API use.
//
// Provided connectionDetails must have Server specified with FQDN to the
// target host.
//
// Optionaly connectionDetails can have user name and password set and Init
// will try to login to the host.
// caCert can be set to use custom CA certificate to validate target host.
func Init(conn *ConnectionDetails) (*HTTPClient, error) {
	caCertPool, err := x509.SystemCertPool()
	if err != nil {
		log.Warn().Msg(err.Error())
	}
	if conn.CAcert != "" {
		caCert, err := os.ReadFile(conn.CAcert)
		if err != nil {
			log.Fatal().Msg(err.Error())
		}
		caCertPool.AppendCertsFromPEM(caCert)
	}
	client := &HTTPClient{
		BaseURL: fmt.Sprintf("https://%s%s", conn.Server, root_path_apiv1),
		Client: &http.Client{
			Timeout: time.Minute,
			Transport: &http.Transport{
				TLSClientConfig: &tls.Config{
					RootCAs:            caCertPool,
					InsecureSkipVerify: conn.Insecure,
				},
			},
		},
	}

	if len(conn.User) > 0 {
		if len(conn.Password) == 0 {
			utils.AskPasswordIfMissing(&conn.Password, "API server password", 0, 0)
		}
		err = client.login(conn)
	}
	return client, err
}

func (c *HTTPClient) login(conn *ConnectionDetails) error {
	url := fmt.Sprintf("%s/%s", c.BaseURL, "auth/login")
	data := map[string]string{
		"login":    conn.User,
		"password": conn.Password,
	}
	jsonData, err := json.Marshal(data)
	if err != nil {
		log.Error().Err(err).Msg("Unable to create login data")
		return err
	}
	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
	if err != nil {
		return err
	}

	res, err := c.sendRequest(req)
	if err != nil {
		return err
	}

	var response map[string]interface{}
	if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
		return err
	}
	if !response["success"].(bool) {
		return fmt.Errorf(response["messages"].(string))
	}

	cookies := res.Cookies()
	for _, cookie := range cookies {
		if cookie.Name == "pxt-session-cookie" && cookie.MaxAge > 0 {
			c.AuthCookie = cookie
			break
		}
	}

	if c.AuthCookie == nil {
		return fmt.Errorf("auth cookie not found in login response")
	}

	return nil
}

// Post issues a POST HTTP request to the API target
//
// `path` specifies an API endpoint
// `data` contains a map of values to add to the POST query. `data` are serialized to the JSON
//
// returns a raw HTTP Response.
func (c *HTTPClient) Post(path string, data map[string]interface{}) (*http.Response, error) {
	url := fmt.Sprintf("%s/%s", c.BaseURL, path)
	jsonData, err := json.Marshal(data)
	if err != nil {
		log.Error().Err(err).Msg("Unable to JSONify data")
		return nil, err
	}

	log.Trace().Msg(string(jsonData))

	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}

	res, err := c.sendRequest(req)
	if err != nil {
		return nil, err
	}

	return res, nil
}

// Get issues GET HTTP request to the API target
//
// `path` specifies API endpoint together with query options
//
// returns a raw HTTP Response.
func (c *HTTPClient) Get(path string) (*http.Response, error) {
	url := fmt.Sprintf("%s/%s", c.BaseURL, path)
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	res, err := c.sendRequest(req)
	if err != nil {
		return nil, err
	}

	return res, nil
}

// Post issues a POST HTTP request to the API target using the client and decodes the response.
//
// `path` specifies an API endpoint
// `data` contains a map of values to add to the POST query. `data` are serialized to the JSON
//
// returns a deserialized JSON data to the map.
func Post[T interface{}](client *HTTPClient, path string, data map[string]interface{}) (*ApiResponse[T], error) {
	res, err := client.Post(path, data)
	if err != nil {
		return nil, err
	}

	defer res.Body.Close()

	var response ApiResponse[T]
	if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
		return nil, err
	}

	return &response, nil
}

// Get issues an HTTP GET request to the API using the client and decodes the response.
//
// `path` specifies API endpoint together with query options
//
// returns an ApiResponse with the decoded result.
func Get[T interface{}](client *HTTPClient, path string) (*ApiResponse[T], error) {
	res, err := client.Get(path)
	if err != nil {
		return nil, err
	}

	defer res.Body.Close()

	var response ApiResponse[T]
	if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
		return nil, err
	}

	return &response, nil
}
070701000000E6000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001B00000000uyuni-tools/shared/api/org070701000000E7000081A4000003E80000006400000001660688CE00000480000000000000000000000000000000000000002A00000000uyuni-tools/shared/api/org/createFirst.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package org

import (
	"errors"
	"fmt"

	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/api/types"
)

// Create first organization and user after initial setup without authentication.
// orgName is the name of the first organization to create and admin the user to create.
func CreateFirst(cnxDetails *api.ConnectionDetails, orgName string, admin *types.User) (*types.Organization, error) {
	client, err := api.Init(cnxDetails)
	if err != nil {
		return nil, fmt.Errorf("failed to connect to the server: %s", err)
	}

	data := map[string]interface{}{
		"orgName":       orgName,
		"adminLogin":    admin.Login,
		"adminPassword": admin.Password,
		"firstName":     admin.FirstName,
		"lastName":      admin.LastName,
		"email":         admin.Email,
	}

	res, err := api.Post[types.Organization](client, "org/createFirst", data)
	if err != nil {
		return nil, fmt.Errorf("failed to create first user and organization: %s", err)
	}

	if !res.Success {
		return nil, errors.New(res.Message)
	}

	return &res.Result, nil
}
070701000000E8000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001D00000000uyuni-tools/shared/api/types070701000000E9000081A4000003E80000006400000001660688CE0000029E000000000000000000000000000000000000002D00000000uyuni-tools/shared/api/types/organization.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package types

// Organization describe an organization in the API.
type Organization struct {
	Id                    int
	Name                  string
	ActiveUsers           int `mapstructure:"active_users"`
	Systems               int
	Trusts                int
	SystemGroups          int  `mapstructure:"system_groups"`
	ActivationKeys        int  `mapstructure:"activation_keys"`
	KickstartProfiles     int  `mapstructure:"kickstart_profiles"`
	ConfigurationChannels int  `mapstructure:"configuration_channels"`
	StagingContentEnabled bool `mapstructure:"staging_content_enabled"`
}
070701000000EA000081A4000003E80000006400000001660688CE000000FE000000000000000000000000000000000000002500000000uyuni-tools/shared/api/types/user.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package types

// User describes an Uyuni user in the API.
type User struct {
	Login     string
	Password  string
	FirstName string
	LastName  string
	Email     string
}
070701000000EB000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001E00000000uyuni-tools/shared/completion070701000000EC000081A4000003E80000006400000001660688CE00000535000000000000000000000000000000000000002C00000000uyuni-tools/shared/completion/completion.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package completion

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// NewCommand  command for generates completion script.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	shellCompletionCmd := &cobra.Command{
		Use:                   "completion [bash|zsh|fish|powershell]",
		Short:                 "Generate shell completion script",
		Long:                  "Generate shell completion script",
		DisableFlagsInUseLine: true,
		ValidArgs:             []string{"bash", "zsh", "fish"},
		Args:                  cobra.ExactValidArgs(1),
		Hidden:                true,
		RunE: func(cmd *cobra.Command, args []string) error {
			switch args[0] {
			case "bash":
				if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil {
					return fmt.Errorf("cannot generate bash completion: %s", err)
				}
			case "zsh":
				if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil {
					return fmt.Errorf("cannot generate zsh completion: %s", err)
				}
			case "fish":
				if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil {
					return fmt.Errorf("cannot generate fish completion: %s", err)
				}
			}
			return nil
		},
	}
	return shellCompletionCmd
}
070701000000ED000081A4000003E80000006400000001660688CE000024CF000000000000000000000000000000000000002100000000uyuni-tools/shared/connection.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package shared

import (
	"bytes"
	"errors"
	"fmt"
	"os/exec"
	"strings"
	"time"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/pflag"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// Connection contains information about how to connect to the server.
type Connection struct {
	backend          string
	command          string
	podName          string
	podmanContainer  string
	kubernetesFilter string
}

// Create a new connection object.
// The backend is either the command to use to connect to the container or the empty string.
//
// The empty strings means automatic detection of the backend where the uyuni container is running.
// podmanContainer is the name of a podman container to look for when detecting the command.
// kubernetesFilter is a filter parameter to use to match a pod.
func NewConnection(backend string, podmanContainer string, kubernetesFilter string) *Connection {
	cnx := Connection{backend: backend, podmanContainer: podmanContainer, kubernetesFilter: kubernetesFilter}

	return &cnx
}

// GetCommand validates or guesses the connection backend command.
func (c *Connection) GetCommand() (string, error) {
	var err error
	if c.command == "" {
		switch c.backend {
		case "podman":
			fallthrough
		case "podman-remote":
			fallthrough
		case "kubectl":
			if _, err = exec.LookPath(c.backend); err != nil {
				err = fmt.Errorf("backend command not found in PATH: %s", c.backend)
			}
			c.command = c.backend
		case "":
			hasPodman := false
			hasKubectl := false

			// Check kubectl with a timeout in case the configured cluster is not responding
			_, err = exec.LookPath("kubectl")
			if err == nil {
				hasKubectl = true
				if out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "--request-timeout=30s", "get", "pod", c.kubernetesFilter, "-A", "-o=jsonpath={.items[*].metadata.name}"); err != nil {
					log.Info().Msg("kubectl not configured to connect to a cluster, ignoring")
				} else if len(bytes.TrimSpace(out)) != 0 {
					c.command = "kubectl"
					return c.command, err
				}
			}

			// Search for other backends
			bins := []string{"podman", "podman-remote"}
			for _, bin := range bins {
				if _, err = exec.LookPath(bin); err == nil {
					hasPodman = true
					if checkErr := utils.RunCmd(bin, "inspect", c.podmanContainer, "--format", "{{.Name}}"); checkErr == nil {
						c.command = bin
						break
					}
				}
			}
			if c.command == "" {
				// Check for uyuni-server.service or helm release
				if hasPodman && podman.HasService("uyuni-server") {
					c.command = "podman"
				} else if hasKubectl {
					clusterInfos, err := kubernetes.CheckCluster()
					if err != nil {
						return c.command, err
					}
					if kubernetes.HasHelmRelease("uyuni", clusterInfos.GetKubeconfig()) {
						c.command = "kubectl"
					}
				}
			}
			if c.command == "" {
				err = fmt.Errorf("uyuni container is not accessible with one of podman, podman-remote or kubectl")
			}
		default:
			err = fmt.Errorf("unsupported backend %s", c.backend)
		}
	}
	return c.command, err
}

// GetPodName finds the name of the running pod.
func (c *Connection) GetPodName() (string, error) {
	var err error

	if c.podName == "" {
		command, cmdErr := c.GetCommand()
		if cmdErr != nil {
			log.Fatal().Err(cmdErr)
		}

		switch command {
		case "podman-remote":
			fallthrough
		case "podman":
			if out, _ := utils.RunCmdOutput(zerolog.DebugLevel, c.command, "ps", "-q", "-f", "name="+c.podmanContainer); len(out) == 0 {
				err = fmt.Errorf("container %s is not running on podman", c.podmanContainer)
			} else {
				c.podName = c.podmanContainer
			}
		case "kubectl":
			// We try the first item on purpose to make the command fail if not available
			podName, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "pod", c.kubernetesFilter, "-A",
				"-o=jsonpath={.items[0].metadata.name}")
			if err == nil {
				c.podName = string(podName[:])
			}
		}
	}

	return c.podName, err
}

// Exec runs command inside the container within an sh shell.
func (c *Connection) Exec(command string, args ...string) ([]byte, error) {
	if c.podName == "" {
		if _, err := c.GetPodName(); c.podName == "" {
			return nil, fmt.Errorf("the container is not running, %s %s command not executed: %s",
				command, strings.Join(args, " "), err)
		}
	}

	cmd, cmdErr := c.GetCommand()
	if cmdErr != nil {
		return nil, cmdErr
	}

	cmdArgs := []string{"exec", c.podName}
	if cmd == "kubectl" {
		cmdArgs = append(cmdArgs, "-c", "uyuni", "--")
	}
	shellArgs := append([]string{command}, args...)
	cmdArgs = append(cmdArgs, shellArgs...)

	return utils.RunCmdOutput(zerolog.DebugLevel, cmd, cmdArgs...)
}

// WaitForServer waits at most 60s for multi-user systemd target to be reached.
func (c *Connection) WaitForServer() error {
	// Wait for the system to be up
	for i := 0; i < 60; i++ {
		podName, err := c.GetPodName()
		if err != nil {
			log.Fatal().Err(err)
		}

		args := []string{"exec", podName}
		command, err := c.GetCommand()
		if err != nil {
			log.Fatal().Err(err)
		}

		if command == "kubectl" {
			args = append(args, "--")
		}
		args = append(args, "systemctl", "is-active", "-q", "multi-user.target")
		output := utils.RunCmd(command, args...)
		isActive := output == nil

		if isActive {
			return nil
		}
		time.Sleep(1 * time.Second)
	}
	return errors.New("server didn't start within 60s. Check for the service status")
}

// Copy transfers a file to or from the container.
// Prefix one of src or dst parameters with `server:` to designate the path is in the container
// user and group parameters are used to set the owner of a file transferred in the container.
func (c *Connection) Copy(src string, dst string, user string, group string) error {
	podName, err := c.GetPodName()
	if err != nil {
		return err
	}
	var commandArgs []string
	extraArgs := []string{}
	srcExpanded := strings.Replace(src, "server:", podName+":", 1)
	dstExpanded := strings.Replace(dst, "server:", podName+":", 1)

	command, err := c.GetCommand()
	if err != nil {
		return err
	}

	switch command {
	case "podman-remote":
		fallthrough
	case "podman":
		commandArgs = []string{"cp", srcExpanded, dstExpanded}
	case "kubectl":
		commandArgs = []string{"cp", "-c", "uyuni", srcExpanded, dstExpanded}
		extraArgs = []string{"-c", "uyuni", "--"}
	default:
		return fmt.Errorf("unknown container kind: %s", command)
	}

	if err := utils.RunCmdStdMapping(zerolog.DebugLevel, command, commandArgs...); err != nil {
		return err
	}

	if user != "" && strings.HasPrefix(dst, "server:") {
		execArgs := []string{"exec", podName}
		execArgs = append(execArgs, extraArgs...)
		owner := user
		if group != "" {
			owner = user + ":" + group
		}
		execArgs = append(execArgs, "chown", owner, strings.Replace(dst, "server:", "", 1))
		return utils.RunCmdStdMapping(zerolog.DebugLevel, command, execArgs...)
	}
	return nil
}

// TestExistenceInPod returns true if dstpath exists in the pod.
func (c *Connection) TestExistenceInPod(dstpath string) bool {
	podName, err := c.GetPodName()
	if err != nil {
		log.Fatal().Err(err)
	}
	commandArgs := []string{"exec", podName}

	command, err := c.GetCommand()
	if err != nil {
		log.Fatal().Err(err)
	}

	switch command {
	case "podman":
		commandArgs = append(commandArgs, "test", "-e", dstpath)
	case "kubectl":
		commandArgs = append(commandArgs, "-c", "uyuni", "test", "-e", dstpath)
	default:
		log.Fatal().Msgf("Unknown container kind: %s\n", command)
	}

	if _, err := utils.RunCmdOutput(zerolog.DebugLevel, command, commandArgs...); err != nil {
		return false
	}
	return true
}

// ChoosePodmanOrKubernetes selects either the podman or the kubernetes function based on the backend.
// This function automatically detects the backend if compiled with kubernetes support and the backend flag is not passed.
func ChoosePodmanOrKubernetes[F interface{}](
	flags *pflag.FlagSet,
	podmanFn utils.CommandFunc[F],
	kubernetesFn utils.CommandFunc[F],
) (utils.CommandFunc[F], error) {
	backend := "podman"
	if utils.KubernetesBuilt {
		backend, _ = flags.GetString("backend")
	}

	cnx := NewConnection(backend, podman.ServerContainerName, kubernetes.ServerFilter)
	return chooseBackend(cnx, podmanFn, kubernetesFn)
}

// ChooseProxyPodmanOrKubernetes selects either the podman or the kubernetes function based on the backend for the proxy.
func ChooseProxyPodmanOrKubernetes[F interface{}](
	flags *pflag.FlagSet,
	podmanFn utils.CommandFunc[F],
	kubernetesFn utils.CommandFunc[F],
) (utils.CommandFunc[F], error) {
	backend, _ := flags.GetString("backend")

	cnx := NewConnection(backend, podman.ProxyContainerNames[0], kubernetes.ProxyFilter)
	return chooseBackend(cnx, podmanFn, kubernetesFn)
}

func chooseBackend[F interface{}](
	cnx *Connection,
	podmanFn utils.CommandFunc[F],
	kubernetesFn utils.CommandFunc[F],
) (utils.CommandFunc[F], error) {
	command, err := cnx.GetCommand()
	if err != nil {
		return nil, fmt.Errorf("failed to determine suitable backend")
	}
	switch command {
	case "podman":
		return podmanFn, nil
	case "kubectl":
		return kubernetesFn, nil
	}

	// Should never happen if the commands are the same than those handled in GetCommand()
	return nil, fmt.Errorf("no supported backend found")
}
070701000000EE000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001E00000000uyuni-tools/shared/kubernetes070701000000EF000081A4000003E80000006400000001660688CE00001085000000000000000000000000000000000000002600000000uyuni-tools/shared/kubernetes/helm.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"os/exec"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// HelmUpgrade runs the helm upgrade command.
//
// To perform an installation, set the install parameter to true: helm would get the --install parameter.
// If repo is not empty, the --repo parameter will be passed.
// If version is not empty, the --version parameter will be passed.
func HelmUpgrade(kubeconfig string, namespace string, install bool,
	repo string, name string, chart string, version string, args ...string) error {
	helmArgs := []string{
		"upgrade",
		"-n", namespace,
		"--create-namespace",
		name,
		chart,
	}
	if kubeconfig != "" {
		helmArgs = append(helmArgs, "--kubeconfig", kubeconfig)
	}

	if repo != "" {
		helmArgs = append(helmArgs, "--repo", repo)
	}
	if version != "" {
		helmArgs = append(helmArgs, "--version", version)
	}
	if install {
		helmArgs = append(helmArgs, "--install")
	}

	helmArgs = append(helmArgs, args...)

	command := "upgrade"
	if install {
		command = "install"
	}
	errorMessage := fmt.Sprintf("Failed to %s helm chart %s in namespace %s", command, chart, namespace)
	if err := utils.RunCmdStdMapping(zerolog.DebugLevel, "helm", helmArgs...); err != nil {
		return fmt.Errorf("%s: %s", errorMessage, err)
	}
	return nil
}

// HelmUninstall runs the helm uninstall command to remove a deployment.
func HelmUninstall(kubeconfig string, deployment string, filter string, dryRun bool) (string, error) {
	helmArgs := []string{}
	if kubeconfig != "" {
		helmArgs = append(helmArgs, "--kubeconfig", kubeconfig)
	}

	jsonpath := fmt.Sprintf("jsonpath={.items[?(@.metadata.name==\"%s\")].metadata.namespace}", deployment)
	args := []string{"get", "-A", "deploy", "-o", jsonpath}
	if filter != "" {
		args = append(args, filter)
	}

	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", args...)
	if err != nil {
		log.Info().Err(err).Msgf("Failed to find %s's namespace, skipping removal", deployment)
	}

	namespace := string(out)
	if namespace == "" {
		log.Debug().Msgf("Pod is not running, trying to find the namespace using the helm release")
		namespace, err = FindNamespace(deployment, kubeconfig)
		if err != nil {
			log.Info().Err(err).Msgf("Cannot guess namespace")
			return "", nil
		}
	}

	if namespace != "" {
		helmArgs = append(helmArgs, "uninstall", "-n", namespace, deployment)

		if dryRun {
			log.Info().Msgf("Would run helm %s", strings.Join(helmArgs, " "))
		} else {
			log.Info().Msgf("Uninstalling %s", deployment)
			if err := utils.RunCmd("helm", helmArgs...); err != nil {
				return namespace, fmt.Errorf("failed to run helm %s: %s", strings.Join(helmArgs, " "), err)
			}
		}
	}
	return namespace, nil
}

// FindNamespace tries to find the deployment namespace using helm.
func FindNamespace(deployment string, kubeconfig string) (string, error) {
	args := []string{}
	if kubeconfig != "" {
		args = append(args, "--kubeconfig", kubeconfig)
	}
	args = append(args, "list", "-aA", "-f", deployment, "-o", "json")
	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "helm", args...)
	if err != nil {
		return "", fmt.Errorf("failed to detect %s's namespace using helm: %s", deployment, err)
	}
	var data []releaseInfo
	if err = json.Unmarshal(out, &data); err != nil {
		return "", fmt.Errorf("helm provided an invalid JSON output: %s", err)
	}

	if len(data) == 1 {
		return data[0].Namespace, nil
	}
	return "", errors.New("found no or more than one deployment")
}

// HasHelmRelease returns whether a helm release is installed or not, even if it failed.
func HasHelmRelease(release string, kubeconfig string) bool {
	if _, err := exec.LookPath("helm"); err == nil {
		args := []string{}
		if kubeconfig != "" {
			args = append(args, "--kubeconfig", kubeconfig)
		}
		args = append(args, "list", "-aAq", "--no-headers", "-f", release)
		out, err := utils.RunCmdOutput(zerolog.TraceLevel, "helm", args...)
		return len(bytes.TrimSpace(out)) != 0 && err == nil
	}
	return false
}

type releaseInfo struct {
	Namespace string `mapstructure:"namespace"`
}
070701000000F0000081A4000003E80000006400000001660688CE0000059F000000000000000000000000000000000000002500000000uyuni-tools/shared/kubernetes/k3s.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"time"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

const k3sTraefikConfigPath = "/var/lib/rancher/k3s/server/manifests/k3s-traefik-config.yaml"

// InstallK3sTraefikConfig install K3s Traefik configuration.
func InstallK3sTraefikConfig(tcpPorts []types.PortMap, udpPorts []types.PortMap) {
	log.Info().Msg("Installing K3s Traefik configuration")

	data := K3sTraefikConfigTemplateData{
		TcpPorts: tcpPorts,
		UdpPorts: udpPorts,
	}
	if err := utils.WriteTemplateToFile(data, k3sTraefikConfigPath, 0600, false); err != nil {
		log.Fatal().Err(err).Msgf("Failed to write K3s Traefik configuration")
	}

	// Wait for traefik to be back
	log.Info().Msg("Waiting for Traefik to be reloaded")
	for i := 0; i < 60; i++ {
		out, err := utils.RunCmdOutput(zerolog.TraceLevel, "kubectl", "get", "job", "-A",
			"-o", "jsonpath={.status.completionTime}", "helm-install-traefik")
		if err == nil {
			completionTime, err := time.Parse(time.RFC3339, string(out))
			if err == nil && time.Since(completionTime).Seconds() < 60 {
				break
			}
		}
	}
}

// UninstallK3sTraefikConfig uninstall K3s Traefik configuration.
func UninstallK3sTraefikConfig(dryRun bool) {
	utils.UninstallFile(k3sTraefikConfigPath, dryRun)
}
070701000000F1000081A4000003E80000006400000001660688CE0000046A000000000000000000000000000000000000003400000000uyuni-tools/shared/kubernetes/k3sTraefikTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"io"
	"text/template"

	"github.com/uyuni-project/uyuni-tools/shared/types"
)

const k3sTraefikConfigTemplate = `apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    ports:
{{- range .TcpPorts }}
      {{ .Name }}:
        port: {{ .Port }}
        expose: true
        exposedPort: {{ .Exposed }}
        protocol: TCP
{{- end }}
{{- range .UdpPorts }}
      {{ .Name }}:
        port: {{ .Port }}
        expose: true
        exposedPort: {{ .Exposed }}
        protocol: UDP
{{- end }}
`

// K3sTraefikConfigTemplateData represents information used to create K3s Traefik helm chart.
type K3sTraefikConfigTemplateData struct {
	TcpPorts []types.PortMap
	UdpPorts []types.PortMap
}

// Render will create the helm chart configuation for K3sTraefik.
func (data K3sTraefikConfigTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("k3sTraefikConfig").Parse(k3sTraefikConfigTemplate))
	return t.Execute(wr, data)
}
070701000000F2000081A4000003E80000006400000001660688CE00000BE0000000000000000000000000000000000000002C00000000uyuni-tools/shared/kubernetes/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"fmt"
	"os"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// ClusterInfos represent cluster information.
type ClusterInfos struct {
	KubeletVersion string
	Ingress        string
}

// IsK3s is true if it's a K3s Cluster.
func (infos ClusterInfos) IsK3s() bool {
	return strings.Contains(infos.KubeletVersion, "k3s")
}

// IsRKE2 is true if it's a RKE2 Cluster.
func (infos ClusterInfos) IsRke2() bool {
	return strings.Contains(infos.KubeletVersion, "rke2")
}

// GetKubeconfig returns the path to the default kubeconfig file or "" if none.
func (infos ClusterInfos) GetKubeconfig() string {
	var kubeconfig string
	if infos.IsK3s() {
		// If the user didn't provide a KUBECONFIG value or file, use the k3s default
		kubeconfigPath := os.ExpandEnv("${HOME}/.kube/config")
		if os.Getenv("KUBECONFIG") == "" || !utils.FileExists(kubeconfigPath) {
			kubeconfig = "/etc/rancher/k3s/k3s.yaml"
		}
	}
	// Since even kubectl doesn't work without a trick on rke2, we assume the user has set kubeconfig
	return kubeconfig
}

// CheckCluster return cluster information.
func CheckCluster() (*ClusterInfos, error) {
	// Get the kubelet version
	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "node",
		"-o", "jsonpath={.items[0].status.nodeInfo.kubeletVersion}")
	if err != nil {
		return nil, fmt.Errorf("failed to get kubelet version: %s", err)
	}

	var infos ClusterInfos
	infos.KubeletVersion = string(out)
	infos.Ingress, err = guessIngress()
	if err != nil {
		return nil, err
	}

	return &infos, nil
}

func guessIngress() (string, error) {
	// Check for a traefik resource
	err := utils.RunCmd("kubectl", "explain", "ingressroutetcp")
	if err == nil {
		return "traefik", nil
	} else {
		log.Debug().Err(err).Msg("No ingressroutetcp resource deployed")
	}

	// Look for a pod running the nginx-ingress-controller: there is no other common way to find out
	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "pod", "-A",
		"-o", "jsonpath={range .items[*]}{.spec.containers[*].args[0]}{.spec.containers[*].command}{end}")
	if err != nil {
		return "", fmt.Errorf("failed to get pod commands to look for nginx controller: %s", err)
	}

	const nginxController = "/nginx-ingress-controller"
	if strings.Contains(string(out), nginxController) {
		return "nginx", nil
	}

	return "", nil
}

// Restart restarts the pod.
func Restart(filter string) error {
	if err := Stop(filter); err != nil {
		return fmt.Errorf("cannot stop %s: %s", filter, err)
	}
	return Start(filter)
}

// Start starts the pod.
func Start(filter string) error {
	// if something is running, we don't need to set replicas to 1
	if _, err := GetNode(filter); err != nil {
		return ReplicasTo(filter, 1)
	}
	log.Debug().Msgf("Already running")
	return nil
}

// Stop stop the pod.
func Stop(filter string) error {
	return ReplicasTo(filter, 0)
}
070701000000F3000081A4000003E80000006400000001660688CE000005B3000000000000000000000000000000000000002600000000uyuni-tools/shared/kubernetes/rke2.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"strconv"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

const rke2NginxConfigPath = "/var/lib/rancher/rke2/server/manifests/rke2-ingress-nginx-config.yaml"

// InstallRke2NgixConfig install Rke2 Nginx configuration.
func InstallRke2NginxConfig(tcpPorts []types.PortMap, udpPorts []types.PortMap, namespace string) {
	log.Info().Msg("Installing RKE2 Nginx configuration")

	data := Rke2NginxConfigTemplateData{
		Namespace: namespace,
		TcpPorts:  tcpPorts,
		UdpPorts:  udpPorts,
	}
	if err := utils.WriteTemplateToFile(data, rke2NginxConfigPath, 0600, false); err != nil {
		log.Fatal().Err(err).Msgf("Failed to write Rke2 nginx configuration")
	}

	// Wait for the nginx controller to be back
	log.Info().Msg("Waiting for Nginx controller to be reloaded")
	for i := 0; i < 60; i++ {
		out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "daemonset", "-A",
			"-o", "jsonpath={.status.numberReady}", "rke2-ingress-nginx-controller")
		if err == nil {
			if count, err := strconv.Atoi(string(out)); err == nil && count > 0 {
				break
			}
		}
	}
}

// UninstallRke2NgixConfig uninstall Rke2 Nginx configuration.
func UninstallRke2NginxConfig(dryRun bool) {
	utils.UninstallFile(rke2NginxConfigPath, dryRun)
}
070701000000F4000081A4000003E80000006400000001660688CE00000444000000000000000000000000000000000000003300000000uyuni-tools/shared/kubernetes/rke2NginxTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"io"
	"text/template"

	"github.com/uyuni-project/uyuni-tools/shared/types"
)

const rke2NginxConfigTemplate = `apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: rke2-ingress-nginx
  namespace: kube-system
spec:
  valuesContent: |-
    controller:
      config:
        hsts: "false"
    tcp:
{{- range .TcpPorts }}
      {{ .Exposed }}: "{{ $.Namespace }}/uyuni-tcp:{{ .Port }}"
{{- end }}
    udp:
{{- range .UdpPorts }}
      {{ .Exposed }}: "{{ $.Namespace }}/uyuni-udp:{{ .Port }}"
{{- end }}
`

// Rke2NginxConfigTemplateData represents information used to create Rke2 Ngix helm chart.
type Rke2NginxConfigTemplateData struct {
	Namespace string
	TcpPorts  []types.PortMap
	UdpPorts  []types.PortMap
}

// Render will create the helm chart configuation for Rke2 Nginx.
func (data Rke2NginxConfigTemplateData) Render(wr io.Writer) error {
	t := template.Must(template.New("rke2NginxConfig").Parse(rke2NginxConfigTemplate))
	return t.Execute(wr, data)
}
070701000000F5000081A4000003E80000006400000001660688CE00000206000000000000000000000000000000000000002B00000000uyuni-tools/shared/kubernetes/uninstall.go// SPDX-FileCopyrightText: 2023 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

// Message appended in the uninstall commands for kubernetes.
const UninstallHelp = `
Note that removing the volumes could also be handled automatically depending on the StorageClass used
when installed on a kubernetes cluster.

For instance on a default K3S install, the local-path-provider storage volumes will
be automatically removed when deleting the deployment even if --purge-volumes argument is not used.`
070701000000F6000081A4000003E80000006400000001660688CE00003012000000000000000000000000000000000000002700000000uyuni-tools/shared/kubernetes/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package kubernetes

import (
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// ServerFilter represents filter used to check server app.
const ServerFilter = "-lapp=uyuni"

// ServerFilter represents filter used to check proxy app.
const ProxyFilter = "-lapp=uyuni-proxy"

// waitForDeployment waits at most 60s for a kubernetes deployment to have at least one replica.
// See [isDeploymentReady] for more details.
func WaitForDeployment(namespace string, name string, appName string) error {
	// Find the name of a replica pod
	// Using the app label is a shortcut, not the 100% acurate way to get from deployment to pod
	podName := ""
	jsonpath := fmt.Sprintf("jsonpath={.items[?(@.metadata.labels.app==\"%s\")].metadata.name}", appName)
	cmdArgs := []string{"get", "pod", "-o", jsonpath}
	cmdArgs = addNamespace(cmdArgs, namespace)

	for i := 0; i < 60; i++ {
		out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...)
		if err == nil {
			podName = string(out)
			break
		}
	}

	// We need to wait for the image to be pulled as this can add quite some time
	// Setting a timeout on this is very hard since it hightly depends on network speed and image size
	// List the Pulled events from the pod as we may not see the Pulling if the image was already downloaded
	err := WaitForPulledImage(namespace, podName)
	if err != nil {
		return fmt.Errorf("failed to pulled image: %s", err)
	}

	log.Info().Msgf("Waiting for %s deployment to be ready in %s namespace\n", name, namespace)
	// Wait for a replica to be ready
	for i := 0; i < 60; i++ {
		// TODO Look for pod failures
		if IsDeploymentReady(namespace, name) {
			return nil
		}
		time.Sleep(1 * time.Second)
	}
	return fmt.Errorf("failed to find a ready replica for deployment %s in namespace %s after 60s", name, namespace)
}

// WaitForPulledImage wait that image is pulled.
func WaitForPulledImage(namespace string, podName string) error {
	log.Info().Msgf("Waiting for image of %s pod in %s namespace to be pulled", podName, namespace)
	pulledArgs := []string{"get", "event",
		"-o", "jsonpath={.items[?(@.reason==\"Pulled\")].message}",
		"--field-selector", "involvedObject.name=" + podName}
	pulledArgs = addNamespace(pulledArgs, namespace)
	failedArgs := []string{"get", "event",
		"-o", "jsonpath={range .items[?(@.reason==\"Failed\")]}{.message}{\"\\n\"}{end}",
		"--field-selector", "involvedObject.name=" + podName}
	failedArgs = addNamespace(failedArgs, namespace)
	for {
		// Look for events indicating an image pull issue
		out, err := utils.RunCmdOutput(zerolog.TraceLevel, "kubectl", failedArgs...)
		if err != nil {
			return fmt.Errorf("failed to get failed events for pod %s", podName)
		}
		lines := strings.Split(string(out), "\n")
		for _, line := range lines {
			if strings.HasPrefix(line, "Failed to pull image") {
				return errors.New("failed to pull image")
			}
		}

		// Has the image pull finished?
		out, err = utils.RunCmdOutput(zerolog.TraceLevel, "kubectl", pulledArgs...)
		if err != nil {
			return fmt.Errorf("failed to get events for pod %s", podName)
		}
		if len(out) > 0 {
			break
		}
		time.Sleep(1 * time.Second)
	}
	return nil
}

// IsDeploymentReady returns true if a kubernetes deployment has at least one ready replica.
// The name can also be a filter parameter like -lapp=uyuni.
// An empty namespace means searching through all the namespaces.
func IsDeploymentReady(namespace string, name string) bool {
	jsonpath := fmt.Sprintf("jsonpath={.items[?(@.metadata.name==\"%s\")].status.readyReplicas}", name)
	args := []string{"get", "-o", jsonpath, "deploy"}
	args = addNamespace(args, namespace)

	out, err := utils.RunCmdOutput(zerolog.TraceLevel, "kubectl", args...)
	// kubectl errors out if the deployment or namespace doesn't exist
	if err == nil {
		if replicas, _ := strconv.Atoi(string(out)); replicas > 0 {
			return true
		}
	}
	return false
}

// DeploymentStatus represents the kubernetes deployment status.
type DeploymentStatus struct {
	AvailableReplicas int
	ReadyReplicas     int
	UpdatedReplicas   int
	Replicas          int
}

// GetDeploymentStatus returns the replicas status of the deployment.
func GetDeploymentStatus(namespace string, name string) (*DeploymentStatus, error) {
	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "deploy", "-n", namespace,
		name, "-o", "jsonpath={.status}")
	if err != nil {
		return nil, err
	}

	var status DeploymentStatus
	if err = json.Unmarshal(out, &status); err != nil {
		return nil, fmt.Errorf("failed to parse deployment status: %s", err)
	}
	return &status, nil
}

// ReplicasTo set the replica for an app to the given value.
// Scale the number of replicas of the server.
func ReplicasTo(filter string, replica uint) error {
	args := []string{"scale", "deploy", filter, "--replicas"}
	log.Debug().Msgf("Setting replicas for pod in %s to %d", filter, replica)
	args = append(args, fmt.Sprint(replica))

	_, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", args...)
	if err != nil {
		return fmt.Errorf("cannot run kubectl %s: %s", args, err)
	}

	pods, err := getPods(filter)
	if err != nil {
		return fmt.Errorf("cannot get pods for %s: %s", filter, err)
	}

	for _, pod := range pods {
		if len(pod) > 0 {
			err = waitForReplica(pod, replica)
			if err != nil {
				return fmt.Errorf("replica to %d failed: %s", replica, err)
			}
		}
	}

	log.Debug().Msgf("Replicas for pod in %s are now %d", filter, replica)

	return err
}

func isPodRunning(podname string, filter string) (bool, error) {
	pods, err := getPods(filter)
	if err != nil {
		return false, fmt.Errorf("cannot check if pod %s is running in app %s: %s", podname, filter, err)
	}
	return utils.Contains(pods, podname), nil
}

func getPods(filter string) (pods []string, err error) {
	log.Debug().Msgf("Checking all pods for %s", filter)
	cmdArgs := []string{"get", "pods", filter, "--output=custom-columns=:.metadata.name", "--no-headers"}
	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...)
	if err != nil {
		return pods, fmt.Errorf("cannot execute %s: %s", strings.Join(cmdArgs, string(" ")), err)
	}
	lines := strings.Split(string(out), "\n")
	pods = append(pods, lines...)
	log.Debug().Msgf("Pods in %s are %s", filter, pods)

	return pods, err
}

func waitForReplicaZero(podname string) error {
	waitSeconds := 120
	cmdArgs := []string{"get", "pod", podname}

	for i := 0; i < waitSeconds; i++ {
		out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...)
		/* Assume that if the command return an error at the first iteration, it's because it failed,
		* next iteration because the pod was actually deleted
		 */
		if err != nil && i == 0 {
			return fmt.Errorf("cannot check for replica zero for %s: %s", podname, err)
		}
		outStr := strings.TrimSuffix(string(out), "\n")
		if len(outStr) == 0 {
			log.Debug().Msgf("Pod %s has been deleted", podname)
			return nil
		}
		time.Sleep(1 * time.Second)
	}
	return fmt.Errorf("cannot set replica for %s to zero", podname)
}

func waitForReplica(podname string, replica uint) error {
	waitSeconds := 120
	log.Debug().Msgf("Checking replica for %s ready to %d", podname, replica)
	if replica == 0 {
		return waitForReplicaZero(podname)
	}
	cmdArgs := []string{"get", "pod", podname, "--output=custom-columns=STATUS:.status.phase", "--no-headers"}

	var err error

	for i := 0; i < waitSeconds; i++ {
		out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...)
		outStr := strings.TrimSuffix(string(out), "\n")
		if err != nil {
			return fmt.Errorf("cannot execute %s: %s", strings.Join(cmdArgs, string(" ")), err)
		}
		if string(outStr) == "Running" {
			log.Debug().Msgf("%s pod replica is now %d", podname, replica)
			break
		}
		log.Debug().Msgf("Pod %s replica is %s in %d seconds.", podname, string(out), i)
		time.Sleep(1 * time.Second)
	}
	if err != nil {
		return fmt.Errorf("pod %s replica is not %d in %s seconds: %s", podname, replica, strconv.Itoa(waitSeconds), err)
	}
	return nil
}

func addNamespace(args []string, namespace string) []string {
	if namespace != "" {
		args = append(args, "-n", namespace)
	} else {
		args = append(args, "-A")
	}
	return args
}

// GetPullPolicy return pullpolicy in lower case, if exists.
func GetPullPolicy(name string) string {
	policies := map[string]string{
		"always":       "Always",
		"never":        "Never",
		"ifnotpresent": "IfNotPresent",
	}
	policy := policies[strings.ToLower(name)]
	if policy == "" {
		log.Fatal().Msgf("%s is not a valid image pull policy value", name)
	}
	return policy
}

// RunPod runs a pod, waiting for its execution and deleting it.
func RunPod(podname string, filter string, image string, pullPolicy string, command string, override ...string) error {
	arguments := []string{"run", podname, "--image", image, "--image-pull-policy", pullPolicy, filter}

	if len(override) > 0 {
		arguments = append(arguments, `--override-type=strategic`)
		for _, arg := range override {
			overrideParam := "--overrides=" + arg
			arguments = append(arguments, overrideParam)
		}
	}

	arguments = append(arguments, "--command", "--", command)
	err := utils.RunCmdStdMapping(zerolog.DebugLevel, "kubectl", arguments...)
	if err != nil {
		return fmt.Errorf("cannot run %s using image %s: %s", command, image, err)
	}
	err = waitForPod(podname)
	if err != nil {
		return fmt.Errorf("deleting pod %s. Status fails with error %s", podname, err)
	}

	defer func() {
		err = DeletePod(podname, filter)
	}()
	return nil
}

// Delete a kubernetes pod named podname.
func DeletePod(podname string, filter string) error {
	isRunning, err := isPodRunning(podname, filter)
	if err != nil {
		return fmt.Errorf("cannot delete pod %s: %s", podname, err)
	}
	if !isRunning {
		log.Debug().Msgf("no need to delete pod %s because is not running", podname)
		return nil
	}
	arguments := []string{"delete", "pod", podname}
	_, err = utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", arguments...)
	if err != nil {
		return fmt.Errorf("cannot delete pod %s: %s", podname, err)
	}
	return nil
}

func waitForPod(podname string) error {
	status := "Succeeded"
	waitSeconds := 120
	log.Debug().Msgf("Checking status for %s pod. Waiting %s seconds until status is %s", podname, strconv.Itoa(waitSeconds), status)
	cmdArgs := []string{"get", "pod", podname, "--output=custom-columns=STATUS:.status.phase", "--no-headers"}
	var err error
	for i := 0; i < waitSeconds; i++ {
		out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...)
		outStr := strings.TrimSuffix(string(out), "\n")
		if err != nil {
			return fmt.Errorf("cannot execute %s: %s", strings.Join(cmdArgs, string(" ")), err)
		}
		if strings.EqualFold(outStr, status) {
			log.Debug().Msgf("%s pod status is %s", podname, status)
			return nil
		}
		if strings.EqualFold(outStr, "Failed") {
			return fmt.Errorf("error during execution of %s: %s", strings.Join(cmdArgs, string(" ")), err)
		}
		log.Debug().Msgf("Pod %s status is %s for %d seconds.", podname, outStr, i)
		time.Sleep(1 * time.Second)
	}
	return fmt.Errorf("pod %s status is not %s in %s seconds: %s", podname, status, strconv.Itoa(waitSeconds), err)
}

// GetNode return the node where the app is running.
func GetNode(filter string) (string, error) {
	nodeName := ""
	cmdArgs := []string{"get", "pod", filter, "-o", "jsonpath={.items[*].spec.nodeName}"}
	for i := 0; i < 60; i++ {
		out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...)
		if err == nil {
			nodeName = string(out)
			break
		}
	}
	if len(nodeName) > 0 {
		log.Debug().Msgf("Node name matching filter %s is: %s", filter, nodeName)
	} else {
		return "", fmt.Errorf("cannot find node name matching filter %s", filter)
	}
	return nodeName, nil
}

// GenerateOverrideDeployment generate a JSON files represents the deployment information.
func GenerateOverrideDeployment(deployData types.Deployment) (string, error) {
	ret, err := json.Marshal(deployData)
	if err != nil {
		return "", fmt.Errorf("cannot marshal deployment %s", err)
	}
	return string(ret), nil
}
070701000000F7000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001800000000uyuni-tools/shared/l10n070701000000F8000081A4000003E80000006400000001660688CE0000013D000000000000000000000000000000000000002300000000uyuni-tools/shared/l10n/gettext.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package l10n

import "github.com/chai2010/gettext-go"

// L localizes a string using the set up gettext domain and locale.
// This is an alias for gettext.Gettext().
func L(message string) string {
	return gettext.Gettext(message)
}
070701000000F9000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001A00000000uyuni-tools/shared/podman070701000000FA000081A4000003E80000006400000001660688CE00001B7F000000000000000000000000000000000000002400000000uyuni-tools/shared/podman/images.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

const rpmImageDir = "/usr/share/suse-docker-images/native/"

var registries = []string{
	"registry.suse.com",
	"registry.opensuse.com",
}

// Ensure the container image is pulled or pull it if the pull policy allows it.
//
// Returns the image name to use. Note that it may be changed if the image has been loaded from a local RPM package.
func PrepareImage(image string, pullPolicy string, args ...string) (string, error) {
	if strings.ToLower(pullPolicy) != "always" {
		log.Info().Msgf("Ensure image %s is available", image)

		presentImage, err := IsImagePresent(image)
		if err != nil {
			return image, err
		}

		if len(presentImage) > 0 {
			log.Debug().Msgf("Image %s already present", presentImage)
			return presentImage, nil
		}
	}

	rpmImageFile := GetRpmImagePath(image)

	if len(rpmImageFile) > 0 {
		log.Debug().Msgf("Image %s present as RPM. Loading it", image)
		loadedImage, err := loadRpmImage(rpmImageFile)
		if err != nil {
			log.Warn().Msgf("Cannot use RPM image for %s: %s", image, err)
		} else {
			log.Info().Msgf("Using the %s image loaded from the RPM instead of its online version %s", strings.TrimSpace(loadedImage), image)
			return loadedImage, nil
		}
	} else {
		log.Info().Msgf("Cannot find RPM image for %s", image)
	}

	if strings.ToLower(pullPolicy) != "never" {
		log.Debug().Msgf("Pulling image %s because it is missing and pull policy is not 'never'", image)
		return image, pullImage(image, args...)
	}

	return image, fmt.Errorf("image %s is missing and cannot be fetched", image)
}

// GetRpmImageName return the RPM Image name and the tag, given an image.
func GetRpmImageName(image string) (rpmImageFile string, tag string) {
	for _, registry := range registries {
		if strings.HasPrefix(image, registry) {
			rpmImageFile = strings.ReplaceAll(image, registry+"/", "")
			rpmImageFile = strings.ReplaceAll(rpmImageFile, "/", "-")
			parts := strings.Split(rpmImageFile, ":")
			tag = "latest"
			if len(parts) > 1 {
				tag = parts[1]
			}
			rpmImageFile = parts[0]
			return rpmImageFile, tag
		}
	}
	return "", ""
}

// BuildRpmImagePath checks the image metadata and returns the RPM Image path.
func BuildRpmImagePath(byteValue []byte, rpmImageFile string, tag string) (string, error) {
	var data types.Metadata
	if err := json.Unmarshal(byteValue, &data); err != nil {
		return "", fmt.Errorf("cannot unmarshal: %s", err)
	}
	fullPathFile := rpmImageDir + data.Image.File
	if data.Image.Name == rpmImageFile {
		for _, metadataTag := range data.Image.Tags {
			if metadataTag == tag {
				return fullPathFile, nil
			}
		}
	}
	return "", nil
}

// GetRpmImagePath return the RPM image path.
func GetRpmImagePath(image string) string {
	log.Debug().Msgf("Looking for installed RPM package containing %s image", image)

	rpmImageFile, tag := GetRpmImageName(image)

	files, err := os.ReadDir(rpmImageDir)
	if err != nil {
		log.Debug().Msgf("Cannot read directory %s: %s", rpmImageDir, err)
		return ""
	}

	for _, file := range files {
		if !strings.HasSuffix(file.Name(), "metadata") {
			continue
		}
		fullPathFileName := path.Join(rpmImageDir, file.Name())
		log.Debug().Msgf("Parsing metadata file %s", fullPathFileName)
		fileHandler, err := os.Open(fullPathFileName)
		if err != nil {
			log.Debug().Msgf("Error opening metadata file %s: %s", fullPathFileName, err)
			continue
		}
		defer fileHandler.Close()
		byteValue, err := io.ReadAll(fileHandler)
		if err != nil {
			log.Debug().Msgf("Error reading metadata file %s: %s", fullPathFileName, err)
			continue
		}

		fullPathFile, err := BuildRpmImagePath(byteValue, rpmImageFile, tag)
		if err != nil {
			log.Warn().Msgf("Cannot unmarshal metadata file %s: %s", fullPathFileName, err)
			return ""
		}
		if len(fullPathFile) > 0 {
			log.Debug().Msgf("%s match with %s", fullPathFileName, image)
			return fullPathFile
		}
		log.Debug().Msgf("%s does not match with %s", fullPathFileName, image)
	}
	log.Debug().Msgf("No installed RPM package containing %s image", image)
	return ""
}

func loadRpmImage(rpmImageBasePath string) (string, error) {
	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "load", "--quiet", "--input", rpmImageBasePath)
	if err != nil {
		return "", err
	}
	parseOutput := strings.SplitN(string(out), ":", 2)
	if len(parseOutput) == 2 {
		return strings.TrimSpace(parseOutput[1]), nil
	}
	return "", fmt.Errorf("error parsing: %s", string(out))
}

// IsImagePresent return true if the image is present.
func IsImagePresent(image string) (string, error) {
	log.Debug().Msgf("Checking for %s", image)
	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "images", "--quiet", image)
	if err != nil {
		return "", fmt.Errorf("failed to check if image %s has already been pulled", image)
	}

	if len(bytes.TrimSpace(out)) > 0 {
		return image, nil
	}

	splitImage := strings.SplitN(string(image), "/", 2)
	if len(splitImage) < 2 {
		return "", nil
	}
	log.Debug().Msgf("Checking for local image of %s", image)
	out, err = utils.RunCmdOutput(zerolog.DebugLevel, "podman", "images", "--quiet", "localhost/"+splitImage[1])
	if err != nil {
		return "", fmt.Errorf("failed to check if image %s has already been pulled", image)
	}
	if len(bytes.TrimSpace(out)) > 0 {
		return "localhost/" + splitImage[1], nil
	}

	return "", nil
}

// GetPulledImageName returns the fullname of a pulled image.
func GetPulledImageName(image string) (string, error) {
	parts := strings.Split(image, "/")
	imageWithTag := parts[len(parts)-1]
	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "images", imageWithTag, "--format", "{{.Repository}}")
	if err != nil {
		return "", fmt.Errorf("failed to check if image %s has already been pulled", parts[len(parts)-1])
	}
	return string(bytes.TrimSpace(out)), nil
}

func pullImage(image string, args ...string) error {
	log.Info().Msgf("Running podman pull %s", image)
	podmanImageArgs := []string{"pull", image}
	podmanArgs := append(podmanImageArgs, args...)

	loglevel := zerolog.DebugLevel
	if len(args) > 0 {
		loglevel = zerolog.Disabled
		log.Debug().Msg("Additional arguments for pull command will not be shown.")
	}

	return utils.RunCmdStdMapping(loglevel, "podman", podmanArgs...)
}

// ShowAvailableTag  returns the list of avaialable tag for a given image.
func ShowAvailableTag(image string) ([]string, error) {
	log.Info().Msgf("Running podman image search --list-tags %s --format='{{.Tag}}'", image)

	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "image", "search", "--list-tags", image, "--format='{{.Tag}}'")
	if err != nil {
		return []string{}, fmt.Errorf("cannot find any tag for image %s: %s", image, err)
	}

	tags := strings.Split(string(out), "\n")
	return tags, nil
}
070701000000FB000081A4000003E80000006400000001660688CE000009B1000000000000000000000000000000000000002900000000uyuni-tools/shared/podman/images_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"testing"
)

func TestGetRpmImageName(t *testing.T) {
	data := [][]string{
		{"suse-manager-5.0-x86_64-proxy-httpd", "latest", "registry.suse.com/suse/manager/5.0/x86_64/proxy-httpd"},
		{"suse-manager-5.0-x86_64-proxy-httpd", "latest", "registry.suse.com/suse/manager/5.0/x86_64/proxy-httpd:latest"},
		{"suse-manager-5.0-x86_64-proxy-httpd", "beta1", "registry.suse.com/suse/manager/5.0/x86_64/proxy-httpd:beta1"},
	}

	for i, testCase := range data {
		rpmImage := testCase[0]
		tag := testCase[1]
		image := testCase[2]

		rpmImageResult, tagResult := GetRpmImageName(image)

		if rpmImage != rpmImageResult {
			t.Errorf("Testcase %d: Expected %s got %s when computing RPM for image %s", i, rpmImage, rpmImageResult, image)
		}
		if tag != tagResult {
			t.Errorf("Testcase %d: Expected %s got %s when computing RPM for image %s", i, tag, tagResult, image)
		}
	}
}

func TestMatchingMetadata(t *testing.T) {
	jsonData := []byte(`{
		"image": {
			"name": "suse-manager-5.0-x86_64-proxy-tftpd",
			"tags": ["latest", "5.0.0-beta1", "5.0.0-beta1.59.128"],
			"file": "suse-manager-5.0-x86_64-proxy-tftpd-latest.x86_64-59.128.tar"
		}
	}`)

	data := [][]string{
		{"/usr/share/suse-docker-images/native/suse-manager-5.0-x86_64-proxy-tftpd-latest.x86_64-59.128.tar", "suse-manager-5.0-x86_64-proxy-httpd", "latest"},
		{"/usr/share/suse-docker-images/native/suse-manager-5.0-x86_64-proxy-tftpd-latest.x86_64-59.128.tar", "suse-manager-5.0-x86_64-proxy-httpd", "5.0.0-beta1.59.128"},
		{"", "suse-manager-5.0-x86_64-proxy-httpd", "missing_tag"},
		{"", "missing_image", "missing_tag"},
		{"", "missing_image", "latest"},
	}

	for i, testCase := range data {
		expectedResult := testCase[0]
		rpmImage := testCase[1]
		tag := testCase[2]

		testResult, err := BuildRpmImagePath(jsonData, rpmImage, tag)

		if err != nil && expectedResult != testResult {
			t.Errorf("Testcase %d: Expected %s got %s when computing RPM for image %s with tag %s", i, expectedResult, testResult, rpmImage, tag)
		}
	}

	jsonDataInvalidWithTypo := []byte(`{
		"image: {
			"name": "suse-manager-5.0-x86_64-proxy-tftpd",
			"tags": ["latest", "5.0.0-beta1", "5.0.0-beta1.59.128"],
			"file": "suse-manager-5.0-x86_64-proxy-tftpd-latest.x86_64-59.128.tar"
		}
	}`)

	_, err := BuildRpmImagePath(jsonDataInvalidWithTypo, "", "")
	if err == nil {
		t.Error("typo in json: this should fail")
	}
}
070701000000FC000081A4000003E80000006400000001660688CE00000DD5000000000000000000000000000000000000002500000000uyuni-tools/shared/podman/network.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"fmt"
	"os/exec"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// The name of the podman network for Uyuni and its proxies.
const UyuniNetwork = "uyuni"

// SetupNetwork creates the podman network.
func SetupNetwork() error {
	log.Info().Msgf("Setting up %s network", UyuniNetwork)

	ipv6Enabled := isIpv6Enabled()

	// check if network exists before trying to get the IPV6 information
	networkExists := IsNetworkPresent(UyuniNetwork)
	if networkExists {
		log.Debug().Msgf("%s network already present", UyuniNetwork)
		// Check if the uyuni network exists and is IPv6 enabled
		hasIpv6, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "network", "inspect", "--format", "{{.IPv6Enabled}}", UyuniNetwork)
		if err == nil {
			if string(hasIpv6) != "true" && ipv6Enabled {
				log.Info().Msgf("%s network doesn't have IPv6, deleting existing network to enable IPv6 on it", UyuniNetwork)
				err := utils.RunCmd("podman", "network", "rm", UyuniNetwork,
					"--log-level", log.Logger.GetLevel().String())
				if err != nil {
					return fmt.Errorf("failed to remove %s podman network: %s", UyuniNetwork, err)
				}
			} else {
				log.Info().Msgf("Reusing existing %s network", UyuniNetwork)
				return nil
			}
		}
	}

	args := []string{"network", "create"}
	if ipv6Enabled {
		// An IPv6 network on a host where IPv6 is disabled doesn't work: don't try it.
		// Check if the networkd backend is netavark
		out, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "info", "--format", "{{.Host.NetworkBackend}}")
		backend := strings.Trim(string(out), "\n")
		if err != nil {
			return fmt.Errorf("failed to find podman's network backend: %s", err)
		} else if backend != "netavark" {
			log.Info().Msgf("Podman's network backend (%s) is not netavark, skipping IPv6 enabling on %s network", backend, UyuniNetwork)
		} else {
			args = append(args, "--ipv6")
		}
	}
	args = append(args, UyuniNetwork)
	err := utils.RunCmd("podman", args...)
	if err != nil {
		return fmt.Errorf("failed to create %s network with IPv6 enabled: %s", UyuniNetwork, err)
	}
	return nil
}

func isIpv6Enabled() bool {
	files := []string{
		"/sys/module/ipv6/parameters/disable",
		"/proc/sys/net/ipv6/conf/default/disable_ipv6",
		"/proc/sys/net/ipv6/conf/all/disable_ipv6",
	}

	for _, file := range files {
		// Mind that we are checking disable files, the semantic is inverted
		if utils.GetFileBoolean(file) {
			return false
		}
	}
	return true
}

// DeleteNetwork deletes the uyuni podman network.
// If dryRun is set to true, nothing will be done, only messages logged to explain what would happen.
func DeleteNetwork(dryRun bool) {
	err := utils.RunCmd("podman", "network", "exists", UyuniNetwork)
	if err != nil {
		log.Info().Msgf("Network %s already removed", UyuniNetwork)
	} else {
		if dryRun {
			log.Info().Msgf("Would run podman network rm %s", UyuniNetwork)
		} else {
			err := utils.RunCmd("podman", "network", "rm", UyuniNetwork)
			if err != nil {
				log.Error().Msgf("Failed to remove network %s", UyuniNetwork)
			} else {
				log.Info().Msg("Network removed")
			}
		}
	}
}

// IsNetworkPresent returns whether a network is already present.
func IsNetworkPresent(network string) bool {
	cmd := exec.Command("podman", "network", "exists", network)
	if err := cmd.Run(); err != nil {
		return false
	}
	return cmd.ProcessState.ExitCode() == 0
}
070701000000FD000081A4000003E80000006400000001660688CE000011B9000000000000000000000000000000000000002500000000uyuni-tools/shared/podman/systemd.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"fmt"
	"os"
	"os/exec"
	"path"

	"github.com/rs/zerolog/log"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

const servicesPath = "/etc/systemd/system/"

// Name of the systemd service for the server.
const ServerService = "uyuni-server"

// Name of the systemd service for the proxy.
const ProxyService = "uyuni-proxy-pod"

// HasService returns if a systemd service is installed.
// name is the name of the service without the '.service' part.
func HasService(name string) bool {
	err := utils.RunCmd("systemctl", "list-unit-files", name+".service")
	return err == nil
}

// GetServicePath return the path for a given service.
func GetServicePath(name string) string {
	return path.Join(servicesPath, name+".service")
}

// UninstallService stops and remove a systemd service.
// If dryRun is set to true, nothing happens but messages are logged to explain what would be done.
func UninstallService(name string, dryRun bool) {
	servicePath := GetServicePath(name)
	if !HasService(name) {
		log.Info().Msgf("Systemd has no %s.service unit", name)
	} else {
		if dryRun {
			log.Info().Msgf("Would run systemctl disable --now %s", name)
			log.Info().Msgf("Would remove %s", servicePath)
		} else {
			log.Info().Msgf("Disable %s service", name)
			// disable server
			err := utils.RunCmd("systemctl", "disable", "--now", name)
			if err != nil {
				log.Error().Err(err).Msgf("Failed to disable %s service", name)
			}

			// Remove the service unit
			log.Info().Msgf("Remove %s", servicePath)
			if err := os.Remove(servicePath); err != nil {
				log.Error().Err(err).Msgf("Failed to remove %s.service file", name)
			}
		}
	}
}

// ReloadDaemon resets the failed state of services and reload the systemd daemon.
// If dryRun is set to true, nothing happens but messages are logged to explain what would be done.
func ReloadDaemon(dryRun bool) error {
	if dryRun {
		log.Info().Msg("Would run systemctl reset-failed")
		log.Info().Msg("Would run systemctl daemon-reload")
	} else {
		err := utils.RunCmd("systemctl", "reset-failed")
		if err != nil {
			return fmt.Errorf("failed to reset-failed systemd")
		}
		err = utils.RunCmd("systemctl", "daemon-reload")
		if err != nil {
			return fmt.Errorf("failed to reload systemd daemon")
		}
	}
	return nil
}

// IsServiceRunning returns whether the systemd service is started or not.
func IsServiceRunning(service string) bool {
	cmd := exec.Command("systemctl", "is-active", "-q", service)
	if err := cmd.Run(); err != nil {
		return false
	}
	return cmd.ProcessState.ExitCode() == 0
}

// RestartService restarts the systemd service.
func RestartService(service string) error {
	if err := utils.RunCmd("systemctl", "restart", service); err != nil {
		return fmt.Errorf("failed to restart systemd %s.service: %s", service, err)
	}
	return nil
}

// StartService starts the systemd service.
func StartService(service string) error {
	if err := utils.RunCmd("systemctl", "start", service); err != nil {
		return fmt.Errorf("failed to start systemd %s.service: %s", service, err)
	}
	return nil
}

// StopService starts the systemd service.
func StopService(service string) error {
	if err := utils.RunCmd("systemctl", "stop", service); err != nil {
		return fmt.Errorf("failed to stop systemd %s.service: %s", service, err)
	}
	return nil
}

// EnableService enables and starts a systemd service.
func EnableService(service string) error {
	if err := utils.RunCmd("systemctl", "enable", "--now", service); err != nil {
		return fmt.Errorf("failed to enable %s systemd service: %s", service, err)
	}
	return nil
}

// Create new systemd service configuration file.
func GenerateSystemdConfFile(serviceName string, section string, body string) error {
	systemdFilePath := GetServicePath(serviceName)
	log.Info().Msgf("systemdFilePath: %s", systemdFilePath)

	systemdConfFolder := systemdFilePath + ".d"
	log.Info().Msgf("systemdConfFolder: %s", systemdConfFolder)
	if err := os.MkdirAll(systemdConfFolder, 0750); err != nil {
		return fmt.Errorf("failed to create %s folder: %s", systemdConfFolder, err)
	}
	systemdConfFilePath := path.Join(systemdConfFolder, section+".conf")
	log.Info().Msgf("systemdConfFilePath: %s", systemdConfFilePath)

	content := []byte("[" + section + "]" + "\n" + body + "\n")
	if err := os.WriteFile(systemdConfFilePath, content, 0644); err != nil {
		return fmt.Errorf("cannot write %s file: %s", systemdConfFilePath, err)
	}

	return nil
}
070701000000FE000081A4000003E80000006400000001660688CE00001241000000000000000000000000000000000000002300000000uyuni-tools/shared/podman/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package podman

import (
	"fmt"
	"os/exec"
	"path"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// ServerContainerName represents the server container name.
const ServerContainerName = "uyuni-server"

// ProxyContainerNames represents all the proxy container names.
var ProxyContainerNames = []string{
	"uyuni-proxy-httpd",
	"uyuni-proxy-salt-broker",
	"uyuni-proxy-squid",
	"uyuni-proxy-ssh",
	"uyuni-proxy-tftpd",
}

// PodmanFlags stores the podman arguments.
type PodmanFlags struct {
	Args   []string         `mapstructure:"arg"`
	Mounts PodmanMountFlags `mapstructure:"mount"`
}

// PodmanMountFlags stores the --podman-mount-* arguments.
type PodmanMountFlags struct {
	Cache      string
	Postgresql string
	Spacewalk  string
}

// AddPodmanInstallFlag add the podman arguments to a command.
func AddPodmanInstallFlag(cmd *cobra.Command) {
	cmd.Flags().StringSlice("podman-arg", []string{}, "Extra arguments to pass to podman")

	cmd.Flags().String("podman-mount-cache", "", "Path to custom /var/cache volume")
	cmd.Flags().String("podman-mount-postgresql", "", "Path to custom /var/lib/pgsql volume")
	cmd.Flags().String("podman-mount-spacewalk", "", "Path to custom /var/spacewalk volume")
}

// EnablePodmanSocket enables the podman socket.
func EnablePodmanSocket() error {
	err := utils.RunCmd("systemctl", "enable", "--now", "podman.socket")
	if err != nil {
		return fmt.Errorf("failed to enable podman.socket unit: %s", err)
	}
	return err
}

// DeleteContainer deletes a container based on its name.
// If dryRun is set to true, nothing will be done, only messages logged to explain what would happen.
func DeleteContainer(name string, dryRun bool) {
	if out, _ := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "ps", "-a", "-q", "-f", "name="+name); len(out) > 0 {
		if dryRun {
			log.Info().Msgf("Would run podman kill %s for container id: %s", name, out)
			log.Info().Msgf("Would run podman remove %s for container id: %s", name, out)
		} else {
			log.Info().Msgf("Run podman kill %s for container id: %s", name, out)
			err := utils.RunCmd("podman", "kill", name)
			if err != nil {
				log.Error().Err(err).Msg("Failed to kill the server")

				log.Info().Msgf("Run podman remove %s for container id: %s", name, out)
				err = utils.RunCmd("podman", "rm", name)
				if err != nil {
					log.Error().Err(err).Msg("Error removing container")
				}
			}
		}
	} else {
		log.Info().Msg("Container already removed")
	}
}

// DeleteVolume deletes a podman volume based on its name.
// If dryRun is set to true, nothing will be done, only messages logged to explain what would happen.
func DeleteVolume(name string, dryRun bool) error {
	exists := isVolumePresent(name)
	if exists {
		if dryRun {
			log.Info().Msgf("Would run podman volume rm %s", name)
		} else {
			log.Info().Msgf("Run podman volume rm %s", name)
			err := utils.RunCmd("podman", "volume", "rm", name)
			if err != nil {
				log.Error().Err(err).Msgf("Failed to remove volume %s", name)
			}
		}
	}
	return nil
}

func isVolumePresent(volume string) bool {
	cmd := exec.Command("podman", "volume", "exists", volume)
	if err := cmd.Run(); err != nil {
		return false
	}
	return cmd.ProcessState.ExitCode() == 0
}

// LinkVolumes adds the symlinks for the podman volumes if needed.
func LinkVolumes(mountFlags *PodmanMountFlags) error {
	graphRoot, err := getGraphRoot()
	if err != nil {
		return err
	}

	data := map[string]string{
		"var-cache":     mountFlags.Cache,
		"var-spacewalk": mountFlags.Spacewalk,
		"var-pgsql":     mountFlags.Postgresql,
	}
	for volume, value := range data {
		if value != "" {
			volumePath := path.Join(graphRoot, "volumes", volume)
			if utils.FileExists(volumePath) {
				return fmt.Errorf("volume folder (%s) already exists, cannot link it to %s", volumePath, value)
			}
			baseFolder := path.Join(graphRoot, "volumes")
			if err := utils.RunCmd("mkdir", "-p", baseFolder); err != nil {
				return fmt.Errorf("failed to create volumes folder: %s: %s", baseFolder, err)
			}

			if err := utils.RunCmd("ln", "-s", value, volumePath); err != nil {
				return fmt.Errorf("failed to link volume folder %s to %s: %s", value, volumePath, err)
			}
		}
	}
	return nil
}

func getGraphRoot() (string, error) {
	out, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "system", "info", "--format", "{{ .Store.GraphRoot }}")
	if err != nil {
		return "", fmt.Errorf("failed to get podman's volumes folder: %s", err)
	}
	return strings.TrimSpace(string(out)), nil
}
070701000000FF000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001900000000uyuni-tools/shared/types07070100000100000081A4000003E80000006400000001660688CE000000FD000000000000000000000000000000000000002200000000uyuni-tools/shared/types/chart.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package types

// ChartFlags represents the flags required by charts.
type ChartFlags struct {
	Namespace string
	Chart     string
	Version   string
	Values    string
}
07070100000101000081A4000003E80000006400000001660688CE000007C5000000000000000000000000000000000000002700000000uyuni-tools/shared/types/deployment.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0
package types

// VolumeMount type used for mapping pod definition structure.
type VolumeMount struct {
	MountPath string `json:"mountPath,omitempty"`
	Name      string `json:"name,omitempty"`
}

// Container type used for mapping pod definition structure.
type Container struct {
	Name         string        `json:"name,omitempty"`
	Image        string        `json:"image,omitempty"`
	VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"`
}

// PersistentVolumeClaim type used for mapping Volume structure.
type PersistentVolumeClaim struct {
	ClaimName string `json:"claimName,omitempty"`
}

// HostPath type used for mapping Volume structure.
type HostPath struct {
	Path string `json:"path,omitempty"`
	Type string `json:"type,omitempty"`
}

// Secret Item for mapping Secret structure.
type SecretItem struct {
	Key  string `json:"key,omitempty"`
	Path string `json:"path,omitempty"`
}

// Secret type for mapping Volume structure.
type Secret struct {
	SecretName string       `json:"secretName,omitempty"`
	Items      []SecretItem `json:"items,omitempty"`
}

// Volume type for mapping Spec structure.
type Volume struct {
	Name                  string                 `json:"name,omitempty"`
	PersistentVolumeClaim *PersistentVolumeClaim `json:"persistentVolumeClaim,omitempty"`
	HostPath              *HostPath              `json:"hostPath,omitempty"`
	Secret                *Secret                `json:"secret,omitempty"`
}

// Spec type for mapping Deployment structure.
type Spec struct {
	NodeName      string      `json:"nodeName,omitempty"`
	RestartPolicy string      `json:"restartPolicy,omitempty"`
	Containers    []Container `json:"containers,omitempty"`
	Volumes       []Volume    `json:"volumes,omitempty"`
}

// Deployment type can store k8s deployment data.
type Deployment struct {
	APIVersion string `json:"apiVersion,omitempty"`
	Spec       *Spec  `json:"spec,omitempty"`
}
07070100000102000081A4000003E80000006400000001660688CE00000126000000000000000000000000000000000000002300000000uyuni-tools/shared/types/distro.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package types

// Distribution contains information about the distribution.
type Distribution struct {
	TreeLabel    string
	BasePath     string
	ChannelLabel string
	InstallType  string
	Arch         string
}
07070100000103000081A4000003E80000006400000001660688CE000000DF000000000000000000000000000000000000002300000000uyuni-tools/shared/types/global.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package types

// GlobalFlags represents the flags used by all commands.
type GlobalFlags struct {
	ConfigPath string
	LogLevel   string
}
07070100000104000081A4000003E80000006400000001660688CE0000025D000000000000000000000000000000000000002300000000uyuni-tools/shared/types/images.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package types

// ImageFlags represents the flags used by an image.
type ImageFlags struct {
	Name       string `mapstructure:"image"`
	Tag        string `mapstructure:"tag"`
	PullPolicy string `mapstructure:"pullPolicy"`
}

// ImageMetadata represents the image metadata of an RPM image.
type ImageMetadata struct {
	Name string   `json:"name"`
	Tags []string `json:"tags"`
	File string   `json:"file"`
}

// Metadata represents the metadata of an RPM image.
type Metadata struct {
	Image ImageMetadata `json:"image"`
}
07070100000105000081A4000003E80000006400000001660688CE00000290000000000000000000000000000000000000002400000000uyuni-tools/shared/types/inspect.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package types

/* InspectData represents CLI command to run in the container
* and the variable where the output is stored.
 */
type InspectData struct {
	Variable string
	CLI      string
}

/* InspectFile represent where the inspect file should be stored
* and the command to run in the container.
 */
type InspectFile struct {
	Directory string
	Basename  string
	Commands  []InspectData
}

// NewInspectData creates an InspectData instance.
func NewInspectData(variable string, cli string) InspectData {
	return InspectData{
		Variable: variable,
		CLI:      cli,
	}
}
07070100000106000081A4000003E80000006400000001660688CE000000D6000000000000000000000000000000000000002500000000uyuni-tools/shared/types/networks.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package types

// PortMap describes a port.
type PortMap struct {
	Name     string
	Exposed  int
	Port     int
	Protocol string
}
07070100000107000041ED000003E80000006400000002660688CE00000000000000000000000000000000000000000000001900000000uyuni-tools/shared/utils07070100000108000081A4000003E80000006400000001660688CE00000930000000000000000000000000000000000000002000000000uyuni-tools/shared/utils/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"fmt"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// Default path where to look for locale files.
//
// On SUSE distros this should be overridden with /usr/share/locale.
var LocaleRoot = "locale"

// DefaultNamespace represents the default name used for image.
var DefaultNamespace = "registry.opensuse.org/uyuni"

// DefaultTag represents the default tag used for image.
var DefaultTag = "latest"

// This variable needs to be set a build time using git tags.
var Version = "0.0.0"

// CommandFunc is a function to be executed by a Cobra command.
type CommandFunc[F interface{}] func(*types.GlobalFlags, *F, *cobra.Command, []string) error

// CommandHelper parses the configuration file into the flags and runs the fn function.
// This function should be passed to Command's RunE.
func CommandHelper[T interface{}](
	globalFlags *types.GlobalFlags,
	cmd *cobra.Command,
	args []string,
	flags *T,
	fn CommandFunc[T],
) error {
	viper, err := ReadConfig(globalFlags.ConfigPath, cmd)
	if err != nil {
		return err
	}
	if err := viper.Unmarshal(&flags); err != nil {
		log.Error().Err(err).Msgf("Failed to unmarshall configuration")
		return fmt.Errorf("failed to unmarshall configuration: %s", err)
	}
	return fn(globalFlags, flags, cmd, args)
}

// AddBackendFlag add the flag for setting the backend ('podman', 'podman-remote', 'kubectl').
func AddBackendFlag(cmd *cobra.Command) {
	cmd.Flags().String("backend", "", L("tool to use to reach the container. Possible values: 'podman', 'podman-remote', 'kubectl'. Default guesses which to use."))
}

// AddPullPolicyFlag adds the --pullPolicy flag to a command.
//
// Since podman doesn't have such a concept of pull policy like kubernetes,
// the values need some explanations for it:
//   - Never: just check and fail if needed
//   - IfNotPresent: check and pull
//   - Always: pull without checking
//
// For kubernetes the value is simply passed to the helm charts.
func AddPullPolicyFlag(cmd *cobra.Command) {
	cmd.Flags().String("pullPolicy", "IfNotPresent",
		"set whether to pull the images or not. The value can be one of 'Never', 'IfNotPresent' or 'Always'")
}
07070100000109000081A4000003E80000006400000001660688CE00000ED5000000000000000000000000000000000000002300000000uyuni-tools/shared/utils/config.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"fmt"
	"os"
	"path"
	"strings"
	"text/template"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
)

const envPrefix = "UYUNI"
const appName = "uyuni-tools"
const configFilename = "config.yaml"

// ReadConfig parse configuration file and env variables a return parameters.
func ReadConfig(configPath string, cmd *cobra.Command) (*viper.Viper, error) {
	v := viper.New()

	v.SetConfigType("yaml")
	v.SetConfigName(configFilename)

	if configPath != "" {
		log.Info().Msgf("Using config file %s", configPath)
		v.SetConfigFile(configPath)
	} else {
		xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
		if xdgConfigHome == "" {
			home, err := os.UserHomeDir()
			if err != nil {
				log.Err(err).Msg("Failed to find home directory")
			} else {
				xdgConfigHome = path.Join(home, ".config")
			}
		}
		if xdgConfigHome != "" {
			v.AddConfigPath(path.Join(xdgConfigHome, appName))
		}
		v.AddConfigPath(".")
	}

	if err := bindFlags(cmd, v); err != nil {
		return nil, err
	}

	if err := v.ReadInConfig(); err != nil {
		// It's okay if there isn't a config file
		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
			// TODO Provide help on the config file format
			return nil, fmt.Errorf("failed to parse configuration file %s: %s", v.ConfigFileUsed(), err)
		}
	}

	v.SetEnvPrefix(envPrefix)

	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

	v.AutomaticEnv()

	return v, nil
}

// Bind each cobra flag to its associated viper configuration (config file and environment variable).
func bindFlags(cmd *cobra.Command, v *viper.Viper) error {
	var errors []error
	cmd.Flags().VisitAll(func(f *pflag.Flag) {
		configName := strings.ReplaceAll(f.Name, "-", ".")
		if err := v.BindPFlag(configName, f); err != nil {
			errors = append(errors, fmt.Errorf("failed to bind %s config to parameter %s: %s", configName, f.Name, err))
		}
	})

	if len(errors) > 0 {
		return errors[0]
	}
	return nil
}

const configTemplate = `
Configuration:

  All the non-global flags can alternatively be passed as configuration.
  
  The configuration file is a YAML file with entries matching the flag name.
  The name of a flag is the part after the '--' of the command line parameter.
  Every '_' character in the flag name means a nested property.
  
  For instance the '--tz CEST' and '--ssl-password secret' will be mapped to
  this YAML configuration:
  
    tz: CEST
    ssl:
      password: secret
  
  The configuration file will be searched in the following places and order:
  · $XDG_CONFIG_HOME/{{ .Name }}/{{ .ConfigFile }}
  · $HOME/.config/{{ .Name }}/{{ .ConfigFile }}
  · $PWD/{{ .ConfigFile }}
  · the value of the --config flag


Environment variables:

  All the non-global flags can also be passed as environment variables.
  
  The environment variable name is the flag name with '-' replaced by with '_'
  and the {{ .EnvPrefix }} prefix.
  
  For example the '--tz CEST' flag will be mapped to '{{ .EnvPrefix }}_TZ'
  and '--ssl-password' flags to '{{ .EnvPrefix }}_SSL_PASSWORD' 
`

// GetUsageWithConfigHelpTemplate returns the usage template with the configuration help added.
func GetUsageWithConfigHelpTemplate(usageTemplate string) (string, error) {
	t := template.Must(template.New("help").Parse(configTemplate))
	var helpBuilder strings.Builder
	if err := t.Execute(&helpBuilder, configTemplateData{
		EnvPrefix:  envPrefix,
		Name:       appName,
		ConfigFile: configFilename,
	}); err != nil {
		return "", fmt.Errorf("cannot return usage template: %s", err)
	}
	return usageTemplate + helpBuilder.String(), nil
}

type configTemplateData struct {
	EnvPrefix  string
	ConfigFile string
	Name       string
}
0707010000010A000081A4000003E80000006400000001660688CE00000A37000000000000000000000000000000000000002100000000uyuni-tools/shared/utils/exec.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"fmt"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/briandowns/spinner"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

// OutputLogWriter contains information output the logger and the loglevel.
type OutputLogWriter struct {
	Logger   zerolog.Logger
	LogLevel zerolog.Level
}

// Write writes a byte array to an OutputLogWriter.
func (l OutputLogWriter) Write(p []byte) (n int, err error) {
	n = len(p)
	if n > 0 && p[n-1] == '\n' {
		// Trim CR added by stdlog.
		p = p[0 : n-1]
	}
	l.Logger.WithLevel(l.LogLevel).CallerSkipFrame(1).Msg(string(p))
	return
}

// RunCmd execute a shell command.
func RunCmd(command string, args ...string) error {
	s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) // Build our new spinner
	s.Suffix = fmt.Sprintf(" %s %s", command, strings.Join(args, " "))
	s.Start() // Start the spinner
	log.Debug().Msgf("Running: %s %s", command, strings.Join(args, " "))
	err := exec.Command(command, args...).Run()
	s.Stop()
	return err
}

// RunCmdStdMapping execute a shell command mapping the stdout and stderr.
func RunCmdStdMapping(logLevel zerolog.Level, command string, args ...string) error {
	s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) // Build our new spinner
	s.Suffix = fmt.Sprintf(" %s %s", command, strings.Join(args, " "))
	if logLevel != zerolog.Disabled {
		s.Start() // Start the spinner
	}
	localLogger := log.Level(logLevel)
	localLogger.Debug().Msgf("Running: %s %s", command, strings.Join(args, " "))

	runCmd := exec.Command(command, args...)
	runCmd.Stdout = os.Stdout
	runCmd.Stderr = os.Stderr
	err := runCmd.Run()
	if logLevel != zerolog.Disabled {
		s.Stop()
	}
	return err
}

// RunCmdOutput execute a shell command and collects output.
func RunCmdOutput(logLevel zerolog.Level, command string, args ...string) ([]byte, error) {
	localLogger := log.Level(logLevel)
	s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) // Build our new spinner
	s.Suffix = fmt.Sprintf(" %s %s", command, strings.Join(args, " "))
	if logLevel != zerolog.Disabled {
		s.Start() // Start the spinner
	}
	localLogger.Debug().Msgf("Running: %s %s", command, strings.Join(args, " "))
	output, err := exec.Command(command, args...).Output()
	if logLevel != zerolog.Disabled {
		s.Stop()
	}
	localLogger.Trace().Msgf("Command output: %s, error: %s", output, err)
	return output, err
}

// IsInstalled checks if a tool is in the path.
func IsInstalled(tool string) bool {
	_, err := exec.LookPath("kubectl")
	return err == nil
}
0707010000010B000081A4000003E80000006400000001660688CE00000103000000000000000000000000000000000000002700000000uyuni-tools/shared/utils/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build !nok8s

package utils

// KubernetesBuilt is a flag for compiling kubernes code. True when go:build !nok8s, False when go:build nok8s.
const KubernetesBuilt = true
0707010000010C000081A4000003E80000006400000001660688CE00000803000000000000000000000000000000000000002500000000uyuni-tools/shared/utils/logUtils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"io"
	"os"
	"path"
	"strconv"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"gopkg.in/natefinch/lumberjack.v2"
)

// LogInit initialize logs.
func LogInit(logToConsole bool) {
	zerolog.CallerMarshalFunc = logCallerMarshalFunction
	zerolog.SetGlobalLevel(zerolog.InfoLevel)

	fileWriter := getFileWriter()
	writers := []io.Writer{fileWriter}
	if logToConsole {
		writers = append(writers, zerolog.NewConsoleWriter())
	}

	multi := zerolog.MultiLevelWriter(writers...)
	log.Logger = zerolog.New(multi).With().Timestamp().Stack().Logger()
}

func getFileWriter() *lumberjack.Logger {
	const globalLogPath = "/var/log/"
	logPath := globalLogPath

	if file, err := os.OpenFile(globalLogPath, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600); err != nil {
		logPath, err = os.UserHomeDir()
		if err != nil {
			logPath = "./"
		}
	} else {
		file.Close()
	}

	fileLogger := &lumberjack.Logger{
		Filename:   path.Join(logPath, "uyuni-tools.log"),
		MaxSize:    5,
		MaxBackups: 5,
		MaxAge:     90,
		Compress:   true,
	}
	return fileLogger
}

// SetLogLevel sets the loglevel.
func SetLogLevel(logLevel string) {
	globalLevel := zerolog.InfoLevel

	level, err := zerolog.ParseLevel(logLevel)
	if logLevel != "" && err == nil {
		globalLevel = level
	}
	if globalLevel <= zerolog.DebugLevel {
		log.Logger = log.Logger.With().Caller().Logger()
	}
	zerolog.SetGlobalLevel(globalLevel)
}

func logCallerMarshalFunction(pc uintptr, file string, line int) string {
	paths := strings.Split(file, "/")
	callerFile := file
	foundSubDir := false
	if strings.HasSuffix(file, "/io/io.go") {
		return "Cmd output"
	}

	for _, currentPath := range paths {
		if foundSubDir {
			if callerFile != "" {
				callerFile = callerFile + "/"
			}
			callerFile = callerFile + currentPath
		} else {
			if strings.Contains(currentPath, "uyuni-tools") {
				foundSubDir = true
				callerFile = ""
			}
		}
	}
	return callerFile + ":" + strconv.Itoa(line)
}
0707010000010D000081A4000003E80000006400000001660688CE00000093000000000000000000000000000000000000002900000000uyuni-tools/shared/utils/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

//go:build nok8s

package utils

const KubernetesBuilt = false
0707010000010E000081A4000003E80000006400000001660688CE00000708000000000000000000000000000000000000002200000000uyuni-tools/shared/utils/ports.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import "github.com/uyuni-project/uyuni-tools/shared/types"

// NewPortMap is a constructor for PortMap type.
func NewPortMap(name string, exposed int, port int) types.PortMap {
	return types.PortMap{
		Name:    name,
		Exposed: exposed,
		Port:    port,
	}
}

// TCP_PORTS are the tcp ports required by the server
// The port names should be less than 15 characters long and lowercased for traefik to eat them.
var TCP_PORTS = []types.PortMap{
	NewPortMap("postgres", 5432, 5432),
	NewPortMap("salt-publish", 4505, 4505),
	NewPortMap("salt-request", 4506, 4506),
	NewPortMap("cobbler", 25151, 25151),
	NewPortMap("psql-mtrx", 9187, 9187),
	NewPortMap("tasko-jmx-mtrx", 5556, 5556),
	NewPortMap("tomcat-jmx-mtrx", 5557, 5557),
	// TODO: Replace Node exporter with cAdvisor
	NewPortMap("node-exporter", 9100, 9100),
	NewPortMap("tasko-mtrx", 9800, 9800),
}

// DEBUG_PORTS are the port used by dev for debugging applications.
var DEBUG_PORTS = []types.PortMap{
	// We can't expose on port 8000 since traefik already uses it
	NewPortMap("tomcat-debug", 8003, 8003),
	NewPortMap("tasko-debug", 8001, 8001),
	NewPortMap("search-debug", 8002, 8002),
}

// UDP_PORTS are the udp ports required by the server.
var UDP_PORTS = []types.PortMap{
	{
		Name:     "tftp",
		Exposed:  69,
		Port:     69,
		Protocol: "udp",
	},
}

// PROXY_TCP_PORTS are the tcp ports required by the proxy.
var PROXY_TCP_PORTS = []types.PortMap{
	NewPortMap("ssh", 8022, 22),
	NewPortMap("salt-publish", 4505, 4505),
	NewPortMap("salt-request", 4506, 4506),
}

// PROXY_PODMAN_PORTS are the http/s ports required by the proxy.
var PROXY_PODMAN_PORTS = []types.PortMap{
	NewPortMap("https", 443, 443),
	NewPortMap("http", 80, 80),
}
0707010000010F000081A4000003E80000006400000001660688CE00000136000000000000000000000000000000000000002300000000uyuni-tools/shared/utils/slices.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

// Contains returns true if a string is contained in a string slice.
func Contains(slice []string, needle string) bool {
	for _, item := range slice {
		if item == needle {
			return true
		}
	}
	return false
}
07070100000110000081A4000003E80000006400000001660688CE00000A6E000000000000000000000000000000000000002000000000uyuni-tools/shared/utils/tar.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"archive/tar"
	"compress/gzip"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/rs/zerolog/log"
)

// Extracts a tar.gz file.
func ExtractTarGz(tarballPath string, dstPath string) error {
	reader, err := os.Open(tarballPath)
	if err != nil {
		return err
	}
	defer reader.Close()

	archive, err := gzip.NewReader(reader)
	if err != nil {
		return err
	}
	defer archive.Close()

	tarReader := tar.NewReader(archive)
	for {
		header, err := tarReader.Next()
		if err == io.EOF {
			break
		} else if err != nil {
			return err
		}

		path, err := filepath.Abs(filepath.Join(dstPath, header.Name))
		if err != nil {
			return err
		}
		if !strings.HasPrefix(path, dstPath) {
			log.Warn().Msgf("Skipping extraction of %s in %s file as is resolves outside the target path",
				header.Name, tarballPath)
			continue
		}

		info := header.FileInfo()
		if info.IsDir() {
			log.Debug().Msgf("Creating folder %s", path)
			if err = os.MkdirAll(path, info.Mode()); err != nil {
				return err
			}
			continue
		}

		log.Debug().Msgf("Extracting file %s", path)
		file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
		if err != nil {
			return err
		}
		defer file.Close()
		if _, err = io.Copy(file, tarReader); err != nil {
			return err
		}
	}

	return nil
}

// Object holding a .tar.gz to write it to a file.
type TarGz struct {
	fileWriter *os.File
	tarWriter  *tar.Writer
	gzipWriter *gzip.Writer
}

// NewTarGz create a targz object with writers opened.
// A successful call should be followed with a close.
func NewTarGz(path string) (*TarGz, error) {
	var targz TarGz
	var err error
	targz.fileWriter, err = os.Create(path)
	if err != nil {
		return nil, fmt.Errorf("failed to write tar.gz to %s: %s", path, err)
	}

	targz.gzipWriter = gzip.NewWriter(targz.fileWriter)
	targz.tarWriter = tar.NewWriter(targz.gzipWriter)
	return &targz, nil
}

// Close stops all the writers.
func (t *TarGz) Close() {
	t.tarWriter.Close()
	t.gzipWriter.Close()
	t.fileWriter.Close()
}

// AddFile adds the file at filepath to the archive as entrypath.
func (t *TarGz) AddFile(filepath string, entrypath string) error {
	file, err := os.Open(filepath)
	if err != nil {
		return err
	}
	defer file.Close()

	info, err := file.Stat()
	if err != nil {
		return err
	}

	header, err := tar.FileInfoHeader(info, info.Name())
	if err != nil {
		return err
	}

	header.Name = entrypath
	if err = t.tarWriter.WriteHeader(header); err != nil {
		return err
	}

	if _, err = io.Copy(t.tarWriter, file); err != nil {
		return err
	}
	return nil
}
07070100000111000081A4000003E80000006400000001660688CE00000E75000000000000000000000000000000000000002500000000uyuni-tools/shared/utils/tar_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"os"
	"os/exec"
	"path"
	"testing"
)

const dataDir = "data"
const outDir = "out"

const file1_content = "file1 content"

var filesData = map[string]string{
	"file1":     file1_content,
	"sub/file2": "file2 content",
}

// Prepare test files to include in the tarball.
func setup(t *testing.T) (string, func(t *testing.T)) {
	dir, err := os.MkdirTemp("", "uyuni-tools-test-")
	if err != nil {
		t.Fatalf("failed to create temporary directory for test: %s", err)
	}

	// Create sub directories for the data and the test
	for _, dirPath := range []string{dataDir, outDir} {
		subDir := path.Join(dir, dirPath)
		if err := os.Mkdir(subDir, 0700); err != nil {
			t.Fatalf("failed to create %s directory: %s", dirPath, err)
		}
	}

	// Add some content to the data directory
	for name, content := range filesData {
		filePath := path.Dir(name)
		if filePath != "." {
			absDir := path.Join(dir, dataDir, filePath)
			if err := os.MkdirAll(absDir, 0700); err != nil {
				t.Fatalf("failed to create subdirectory %s for test: %s", absDir, err)
			}
		}
		if err := os.WriteFile(path.Join(dir, dataDir, name), []byte(content), 0700); err != nil {
			t.Fatalf("failed to write test data file %s: %s", name, err)
		}
	}

	// Returns the teardown function.
	return dir, func(t *testing.T) {
		if err := os.RemoveAll(dir); err != nil {
			t.Logf("failed to clean test directory: %s", err)
		}
	}
}

func TestWriteTarGz(t *testing.T) {
	tmpDir, teardown := setup(t)
	defer teardown(t)

	// Create the tarball
	tarballPath := path.Join(tmpDir, "test.tar.gz")
	tarball, err := NewTarGz(tarballPath)
	if err != nil {
		t.Fatalf("failed to create tarball: %s", err)
	}
	if err := tarball.AddFile(path.Join(tmpDir, dataDir, "file1"), "otherfile1"); err != nil {
		t.Fatalf("failed to add file1 to tarball: %s", err)
	}
	if err := tarball.AddFile(path.Join(tmpDir, dataDir, "sub/file2"), "sub/file2"); err != nil {
		t.Fatalf("failed to add sub/file2 to tarball: %s", err)
	}
	tarball.Close()

	// Check the tarball using the tar utility
	testDir := path.Join(tmpDir, outDir)
	if out, err := exec.Command("tar", "xzf", tarballPath, "-C", testDir).CombinedOutput(); err != nil {
		t.Fatalf("failed to extract generated tarball: %s", string(out))
	}

	// Ensure we have all expected files
	for _, file := range []string{"otherfile1", "sub/file2"} {
		if !FileExists(path.Join(testDir, file)) {
			t.Errorf("Missing %s in archive", file)
		}
	}

	// Check the content of a file
	if out, err := os.ReadFile(path.Join(testDir, "otherfile1")); err != nil {
		t.Errorf("failed to read otherfile1: %s", err)
	} else if string(out) != file1_content {
		t.Errorf("expected otherfile1 content %s, but got %s", file1_content, string(out))
	}
}

func TestExtractTarGz(t *testing.T) {
	tmpDir, teardown := setup(t)
	defer teardown(t)

	// Create an archive using the tar tool
	tarballPath := path.Join(tmpDir, "test.tar.gz")
	dataPath := path.Join(tmpDir, dataDir)
	if out, err := exec.Command("tar", "czf", tarballPath, "-C", dataPath, ".").CombinedOutput(); err != nil {
		t.Fatalf("failed to create test tar.gz: %s", string(out))
	}

	// Extract the tarball
	testDir := path.Join(tmpDir, outDir)
	if err := ExtractTarGz(tarballPath, testDir); err != nil {
		t.Errorf("Failed to extract tar.gz: %s", err)
	}

	// Check the extracted content
	for name, content := range filesData {
		if out, err := os.ReadFile(path.Join(testDir, name)); err != nil {
			t.Errorf("failed to read %s: %s", name, err)
		} else if string(out) != content {
			t.Errorf("expected %s content %s, but got %s", name, content, string(out))
		}
	}
}
07070100000112000081A4000003E80000006400000001660688CE00000332000000000000000000000000000000000000002500000000uyuni-tools/shared/utils/template.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"io"
	"os"

	"github.com/rs/zerolog/log"
)

// Template is an interface for implementing Render function.
type Template interface {
	Render(wr io.Writer) error
}

// WriteTemplateToFile writes a template to a file.
func WriteTemplateToFile(template Template, path string, perm os.FileMode, overwrite bool) error {
	// Check if the file is existing
	if !overwrite {
		if FileExists(path) {
			log.Fatal().Msgf("%s file already present, not overwriting", path)
		}
	}

	// Write the configuration
	file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
	if err != nil {
		log.Fatal().Err(err).Msgf("Failed to open %s for writing", path)
	}
	defer file.Close()

	return template.Render(file)
}
07070100000113000081A4000003E80000006400000001660688CE0000118C000000000000000000000000000000000000002200000000uyuni-tools/shared/utils/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"bufio"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"os"
	"regexp"
	"strings"
	"syscall"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"golang.org/x/term"
)

const prompt_end = ": "

func checkValueSize(value string, min int, max int) bool {
	if min == 0 && max == 0 {
		return true
	}

	if len(value) < min {
		fmt.Printf("Has to be more than %d characters long", min)
		return false
	}
	if len(value) > max {
		fmt.Printf("Has to be less than %d characters long", max)
		return false
	}
	return true
}

// AskPasswordIfMissing asks for password if missing.
// Don't perform any check if min and max are set to 0.
func AskPasswordIfMissing(value *string, prompt string, min int, max int) {
	for *value == "" {
		fmt.Print(prompt + prompt_end)
		bytePassword, err := term.ReadPassword(int(syscall.Stdin))
		if err != nil {
			log.Fatal().Err(err).Msgf("Failed to read password")
		}
		tmpValue := strings.TrimSpace(string(bytePassword))
		r := regexp.MustCompile(`^[^\t ]+$`)
		validChars := r.MatchString(tmpValue)
		if !validChars {
			fmt.Printf("Cannot contain spaces or tabs")
		}

		if validChars && checkValueSize(tmpValue, min, max) {
			*value = tmpValue
		}
		fmt.Println()
		if *value == "" {
			fmt.Println("A value is required")
		}
	}
}

// AskIfMissing asks for a value if missing.
// Don't perform any check if min and max are set to 0.
func AskIfMissing(value *string, prompt string, min int, max int, checker func(string) bool) {
	reader := bufio.NewReader(os.Stdin)
	for *value == "" {
		fmt.Print(prompt + prompt_end)
		newValue, err := reader.ReadString('\n')
		if err != nil {
			log.Fatal().Err(err).Msgf("Failed to read input")
		}
		tmpValue := strings.TrimSpace(newValue)
		if checkValueSize(tmpValue, min, max) && (checker == nil || checker(tmpValue)) {
			*value = tmpValue
		}
		fmt.Println()
		if *value == "" {
			fmt.Println("A value is required")
		}
	}
}

// ComputeImage assembles the container image from its name and tag.
func ComputeImage(name string, tag string, appendToName ...string) (string, error) {
	imageValid := regexp.MustCompile("^((?:[^:/]+(?::[0-9]+)?/)?[^:]+)(?::([^:]+))?$")
	submatches := imageValid.FindStringSubmatch(name)
	if submatches == nil {
		return "", fmt.Errorf("invalid image name: %s", name)
	}
	if submatches[2] == `` {
		if len(tag) <= 0 {
			return name, fmt.Errorf("tag missing on %s", name)
		}
		if len(appendToName) > 0 {
			name = name + strings.Join(appendToName, ``)
		}
		// No tag provided in the URL name, append the one passed
		imageName := fmt.Sprintf("%s:%s", name, tag)
		log.Debug().Msgf("Computed image name is %s", imageName)
		return imageName, nil
	}
	imageName := submatches[1] + strings.Join(appendToName, ``) + `:` + submatches[2]
	log.Debug().Msgf("Computed image name is %s", imageName)
	return imageName, nil
}

// Get the timezone set on the machine running the tool.
func GetLocalTimezone() string {
	out, err := RunCmdOutput(zerolog.DebugLevel, "timedatectl", "show", "--value", "-p", "Timezone")
	if err != nil {
		log.Fatal().Err(err).Msgf("Failed to run timedatectl show --value -p Timezone")
	}
	return string(out)
}

// Check if a given path exists.
func FileExists(path string) bool {
	_, err := os.Stat(path)
	if err == nil {
		return true
	} else if !os.IsNotExist(err) {
		log.Fatal().Err(err).Msgf("Failed to stat %s file", path)
	}
	return false
}

// Returns the content of a file and exit if there was an error.
func ReadFile(file string) []byte {
	out, err := os.ReadFile(file)
	if err != nil {
		log.Fatal().Err(err).Msgf("Failed to read file %s", file)
	}
	return out
}

// Get the value of a file containing a boolean.
// This is handy for files from the kernel API.
func GetFileBoolean(file string) bool {
	return string(ReadFile(file)) != "0"
}

// Uninstalls a file.
func UninstallFile(path string, dryRun bool) {
	if FileExists(path) {
		if dryRun {
			log.Info().Msgf("Would remove file %s", path)
		} else {
			log.Info().Msgf("Removing file %s", path)
			if err := os.Remove(path); err != nil {
				log.Info().Err(err).Msgf("Failed to remove file %s", path)
			}
		}
	}
}

// GetRandomBase64 generates random base64-encoded data.
func GetRandomBase64(size int) string {
	data := make([]byte, size)
	if _, err := rand.Read(data); err != nil {
		log.Fatal().Err(err).Msg("Failed to read random data")
	}
	return base64.StdEncoding.EncodeToString(data)
}
07070100000114000081A4000003E80000006400000001660688CE00001523000000000000000000000000000000000000002700000000uyuni-tools/shared/utils/utils_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"fmt"
	"os"
	"regexp"
	"strings"
	"syscall"
	"testing"

	expect "github.com/Netflix/go-expect"
)

type askTestData struct {
	value           string
	expectedMessage string
	min             int
	max             int
	checker         func(string) bool
}

func TestAskIfMissing(t *testing.T) {
	c, err := expect.NewConsole(expect.WithStdout(os.Stdout))
	if err != nil {
		t.Errorf("Failed to create fake console")
	}
	defer c.Close()

	origStdin := os.Stdin
	origStdout := os.Stdout

	os.Stdin = c.Tty()
	os.Stdout = c.Tty()
	defer func() {
		os.Stdin = origStdin
		os.Stdout = origStdout
	}()

	fChecker := func(v string) bool {
		if !strings.Contains(v, "f") {
			fmt.Println("Has to contain an 'f'")
			return false
		}
		return true
	}

	data := []askTestData{
		{value: "\n", expectedMessage: "A value is required", min: 1, max: 5, checker: nil},
		{value: "superlong\n", expectedMessage: "Has to be less than 5 characters long", min: 1, max: 5, checker: nil},
		{value: "a\n", expectedMessage: "Has to be more than 2 characters long", min: 2, max: 5, checker: nil},
		{value: "booh\n", expectedMessage: "Has to contain an 'f'", min: 0, max: 0, checker: fChecker},
	}

	for i, testCase := range data {
		go func() {
			if _, err := c.ExpectString("Prompted value: "); err != nil {
				t.Errorf("Testcase %d: Expected prompt error: %s", i, err)
			}
			if _, err := c.Send(testCase.value); err != nil {
				t.Errorf("Testcase %d: Failed to send value to fake console: %s", i, err)
			}
			if _, err := c.Expect(expect.Regexp(regexp.MustCompile(testCase.expectedMessage))); err != nil {
				t.Errorf("Testcase %d: Expected '%s' message: %s", i, testCase.expectedMessage, err)
			}
			if _, err := c.ExpectString("Prompted value: "); err != nil {
				t.Errorf("Testcase %d: Expected prompt error: %s", i, err)
			}
			if _, err := c.Send("foo\n"); err != nil {
				t.Errorf("Testcase %d: Failed to send value to fake console: %s", i, err)
			}
		}()

		var value string
		AskIfMissing(&value, "Prompted value", testCase.min, testCase.max, testCase.checker)
		if value != "foo" {
			t.Errorf("Testcase %d: Expected 'foo', got '%s' value", i, value)
		}
	}
}

func TestAskPasswordIfMissing(t *testing.T) {
	c, err := expect.NewConsole(expect.WithStdout(os.Stdout))
	if err != nil {
		t.Errorf("Failed to create fake console")
	}
	defer c.Close()

	origStdin := syscall.Stdin
	origStdout := os.Stdout

	syscall.Stdin = int(c.Tty().Fd())
	os.Stdout = c.Tty()
	defer func() {
		syscall.Stdin = origStdin
		os.Stdout = origStdout
	}()

	data := []askTestData{
		{value: "\n", expectedMessage: "A value is required", min: 1, max: 5, checker: nil},
		{value: "superlong\n", expectedMessage: "Has to be less than 5 characters long", min: 1, max: 5, checker: nil},
		{value: "a\n", expectedMessage: "Has to be more than 2 characters long", min: 2, max: 5, checker: nil},
	}

	for i, testCase := range data {
		go func() {
			if _, err := c.ExpectString("Prompted password: "); err != nil {
				t.Errorf("Testcase %d: Expected prompt error: %s", i, err)
			}
			if _, err := c.Send(testCase.value); err != nil {
				t.Errorf("Testcase %d: Failed to send value to fake console: %s", i, err)
			}
			if _, err := c.Expect(expect.Regexp(regexp.MustCompile(testCase.expectedMessage))); err != nil {
				t.Errorf("Testcase %d: Expected '%s' message: %s", i, testCase.expectedMessage, err)
			}
			if _, err := c.ExpectString("Prompted password: "); err != nil {
				t.Errorf("Testcase %d: Expected prompt error: %s", i, err)
			}
			if _, err := c.Send("foo\n"); err != nil {
				t.Errorf("Testcase %d: Failed to send value to fake console: %s", i, err)
			}
		}()

		var value string
		AskPasswordIfMissing(&value, "Prompted password", testCase.min, testCase.max)
		if value != "foo" {
			t.Errorf("Expected 'foo', got '%s' value", value)
		}
	}
}

func TestComputeImage(t *testing.T) {
	data := [][]string{
		{"registry:5000/path/to/image:foo", "registry:5000/path/to/image:foo", "bar"},
		{"registry:5000/path/to/image:bar", "registry:5000/path/to/image", "bar"},
		{"registry/path/to/image:foo", "registry/path/to/image:foo", "bar"},
		{"registry/path/to/image:bar", "registry/path/to/image", "bar"},
		{"registry:5000/path/to/image-migration-14-16:foo", "registry:5000/path/to/image:foo", "bar", "-migration-14-16"},
		{"registry:5000/path/to/image-migration-14-16:bar", "registry:5000/path/to/image", "bar", "-migration-14-16"},
		{"registry/path/to/image-migration-14-16:foo", "registry/path/to/image:foo", "bar", "-migration-14-16"},
		{"registry/path/to/image-migration-14-16:bar", "registry/path/to/image", "bar", "-migration-14-16"},
	}

	for i, testCase := range data {
		result := testCase[0]
		image := testCase[1]
		tag := testCase[2]
		appendToImage := testCase[3:]

		actual, err := ComputeImage(image, tag, appendToImage...)

		if err != nil {
			t.Errorf("Testcase %d: Unexpected error while computing image with %s, %s, %s: %s", i, image, tag, appendToImage, err)
		}
		if actual != result {
			t.Errorf("Testcase %d: Expected %s got %s when computing image with %s, %s, %s", i, result, actual, image, tag, appendToImage)
		}
	}
}

func TestComputeImageError(t *testing.T) {
	_, err := ComputeImage("registry:path/to/image:tag:tag", "bar")
	if err == nil {
		t.Error("Expected error, got none")
	}
}
07070100000115000081A4000003E80000006400000001660688CE00001743000000000000000000000000000000000000002400000000uyuni-tools/shared/utils/volumes.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package utils

import "github.com/uyuni-project/uyuni-tools/shared/types"

// PgsqlRequiredVolumeMounts represents volumes mount used by PostgreSQL.
var PgsqlRequiredVolumeMounts = []types.VolumeMount{
	{MountPath: "/etc/pki/tls", Name: "etc-tls"},
	{MountPath: "/var/lib/pgsql", Name: "var-pgsql"},
	{MountPath: "/etc/rhn", Name: "etc-rhn"},
	{MountPath: "/etc/pki/spacewalk-tls", Name: "tls-key"},
}

// PgsqlRequiredVolumes represents volumes used by PostgreSQL.
var PgsqlRequiredVolumes = []types.Volume{
	{Name: "etc-tls", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-tls"}},
	{Name: "var-pgsql", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-pgsql"}},
	{Name: "etc-rhn", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-rhn"}},
	{Name: "tls-key",
		Secret: &types.Secret{
			SecretName: "uyuni-cert", Items: []types.SecretItem{
				{Key: "tls.crt", Path: "spacewalk.crt"},
				{Key: "tls.key", Path: "spacewalk.key"},
			},
		},
	},
}

// EtcServerVolumeMounts represents volumes mounted in /etc folder.
var EtcServerVolumeMounts = []types.VolumeMount{
	{MountPath: "/etc/apache2", Name: "etc-apache2"},
	{MountPath: "/etc/systemd/system/multi-user.target.wants", Name: "etc-systemd-multi"},
	{MountPath: "/etc/systemd/system/sockets.target.wants", Name: "etc-systemd-sockets"},
	{MountPath: "/etc/salt", Name: "etc-salt"},
	{MountPath: "/etc/rhn", Name: "etc-rhn"},
	{MountPath: "/etc/tomcat", Name: "etc-tomcat"},
	{MountPath: "/etc/cobbler", Name: "etc-cobbler"},
	{MountPath: "/etc/sysconfig", Name: "etc-sysconfig"},
	{MountPath: "/etc/postfix", Name: "etc-postfix"},
}

// EtcServerVolumeMounts represents volumes used for configuration.
var EtcServerVolumes = []types.Volume{
	{Name: "etc-apache2", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-apache2"}},
	{Name: "etc-systemd-multi", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-systemd-multi"}},
	{Name: "etc-systemd-sockets", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-systemd-sockets"}},
	{Name: "etc-salt", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-salt"}},
	{Name: "etc-tomcat", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-tomcat"}},
	{Name: "etc-cobbler", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-cobbler"}},
	{Name: "etc-sysconfig", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-sysconfig"}},
	{Name: "etc-postfix", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-postfix"}},
	{Name: "etc-rhn", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-rhn"}},
}

var etcAndPgsqlVolumeMounts = append(PgsqlRequiredVolumeMounts, EtcServerVolumeMounts[:]...)
var etcAndPgsqlVolumes = append(PgsqlRequiredVolumes, EtcServerVolumes[:]...)

// ServerVolumeMounts should match the volumes mapping from the container definition in both
// the helm chart and the systemctl services definitions.
var ServerVolumeMounts = append([]types.VolumeMount{
	{MountPath: "/var/lib/cobbler", Name: "var-cobbler"},
	{MountPath: "/var/lib/salt", Name: "var-salt"},
	{MountPath: "/var/cache", Name: "var-cache"},
	{MountPath: "/var/spacewalk", Name: "var-spacewalk"},
	{MountPath: "/var/log", Name: "var-log"},
	{MountPath: "/srv/salt", Name: "srv-salt"},
	{MountPath: "/srv/www/", Name: "srv-www"},
	{MountPath: "/srv/tftpboot", Name: "srv-tftpboot"},
	{MountPath: "/srv/formula_metadata", Name: "srv-formulametadata"},
	{MountPath: "/srv/pillar", Name: "srv-pillar"},
	{MountPath: "/srv/susemanager", Name: "srv-susemanager"},
	{MountPath: "/srv/spacewalk", Name: "srv-spacewalk"},
	{MountPath: "/root", Name: "root"},
	{MountPath: "/etc/pki/trust/anchors", Name: "ca-cert"},
}, etcAndPgsqlVolumeMounts[:]...)

// ServerVolumes match the volume with Persistent Volume Claim.
var ServerVolumes = append([]types.Volume{
	{Name: "var-cobbler", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-cobbler"}},
	{Name: "var-salt", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-salt"}},
	{Name: "var-cache", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-cache"}},
	{Name: "var-spacewalk", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-spacewalk"}},
	{Name: "var-log", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-log"}},
	{Name: "srv-salt", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-salt"}},
	{Name: "srv-www", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-www"}},
	{Name: "srv-tftpboot", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-tftpboot"}},
	{Name: "srv-formulametadata", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-formulametadata"}},
	{Name: "srv-pillar", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-pillar"}},
	{Name: "srv-susemanager", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-susemanager"}},
	{Name: "srv-spacewalk", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-spacewalk"}},
	{Name: "root", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "root"}},
	{Name: "ca-cert", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "ca-cert"}},
}, etcAndPgsqlVolumes[:]...)

// PROXY_HTTPD_VOLUMES volumes used by HTTPD in proxy.
var PROXY_HTTPD_VOLUMES = map[string]string{
	"uyuni-proxy-rhn-cache": "/var/cache/rhn",
	"uyuni-proxy-tftpboot":  "/srv/tftpboot",
}

// PROXY_HTTPD_VOLUMES volumes used by Squid in  proxy.
var PROXY_SQUID_VOLUMES = map[string]string{
	"uyuni-proxy-squid-cache": "/var/cache/squid",
}

// PROXY_TFTPD_VOLUMES volumes used by TFTP in proxy.
var PROXY_TFTPD_VOLUMES = map[string]string{
	"uyuni-proxy-tftpboot": "/srv/tftpboot:ro",
}
07070100000116000081A4000003E80000006400000001660688CE000008E6000000000000000000000000000000000000002000000000uyuni-tools/uyuni-tools.changes-------------------------------------------------------------------
Tue Feb 27 14:50:42 CET 2024 - marina.latini@suse.com

- version 0.1.5-0
  * Install aardvark-dns if netavark is installed (bsc#1220371)

-------------------------------------------------------------------
Tue Feb 13 18:45:11 CET 2024 - marina.latini@suse.com

- version 0.1.4-1
  * Add mgradm start stop and restart commands
  * Do not build fish shell completion on Red Hat Enterprise Linux
    and clones
  * Stop services and database in podman server gracefully
  * tomcat and taskomatic should listen on all interfaces also in podman case

-------------------------------------------------------------------
Wed Jan 31 14:56:34 CET 2024 - rosuna@suse.com

- version 0.1.3-1
  * Add configuration help
  * Add a warning message for interactive shell
  * Accept image URLs with the tag already appended
  * Add mgradm supportconfig command
  * Verify if podman, kubectl or helm are installed before using them
  * Add migration of config files
  * Disable SELinux relabeling by Podman for migration container.
    Fixes SELinux access problems for SSH agent socket.
  * FQDN optional in command install for Podman

-------------------------------------------------------------------
Mon Jan 15 11:08:45 CET 2024 - marina.latini@suse.com

- version 0.1.2-1
  * Adapt the build tags also in the spec file

-------------------------------------------------------------------
Thu Jan 11 16:49:18 CET 2024 - marina.latini@suse.com

- version 0.1.1-1
  * Use tito for releasing
  * Use the latest git tag as version instead of hardcoding it
  * Comply to reuse.software rules for license documentation
  * Add shell autocompletions
  * Rename the tools to mgradm and mgrctl
  * Add postgres migration
  * Add migration of autoinstallable distributions
  * Add mgrpxy tool with install and uninstall subcommands
  * Merge /srv/www/ volumes and add one for /var/lib/salt
  * Build uyuniadm also for Tumbleweed and ALP

-------------------------------------------------------------------
Tue Oct 24 13:24:46 UTC 2023 - Michele Bussolotto <michele.bussolotto@suse.com>

- Initial packaging of uyuni-tools 0.0.3
  * Create uyuniadm and uyunictl packages
  * Make it possible to build uyuniadm only on specific distro
07070100000117000081A4000003E80000006400000001660688CE00000052000000000000000000000000000000000000003300000000uyuni-tools/uyuni-tools.changes.cbosdo.kubelet-fix- Don't assume the current host is a cluster node when getting 
  kubelet version
07070100000118000081A4000003E80000006400000001660688CE0000002E000000000000000000000000000000000000003100000000uyuni-tools/uyuni-tools.changes.cbosdo.pxy-start- Add mgrpxy start, stop and restart commands
07070100000119000081A4000003E80000006400000001660688CE0000002C000000000000000000000000000000000000003000000000uyuni-tools/uyuni-tools.changes.cbosdo.shm-size- Remove shm size constraints on the server
0707010000011A000081A4000003E80000006400000001660688CE00000028000000000000000000000000000000000000002E00000000uyuni-tools/uyuni-tools.changes.cbosdo.status- Add mgrpxy and mgradm status commands
0707010000011B000081A4000003E80000006400000001660688CE00000048000000000000000000000000000000000000003700000000uyuni-tools/uyuni-tools.changes.cbosdo.uninstall-force- uninstall commands dry run by default to avoid unintended 
  removals
0707010000011C000081A4000003E80000006400000001660688CE00000025000000000000000000000000000000000000003800000000uyuni-tools/uyuni-tools.changes.cbosdonnat.auto-db-pass- Autogenerate the database password
0707010000011D000081A4000003E80000006400000001660688CE0000002C000000000000000000000000000000000000004000000000uyuni-tools/uyuni-tools.changes.cbosdonnat.mandatory-first-user- Make first user mandatory at install time
0707010000011E000081A4000003E80000006400000001660688CE0000001A000000000000000000000000000000000000003700000000uyuni-tools/uyuni-tools.changes.cbosdonnat.mgrctl-term- Add mgrctl term command
0707010000011F000081A4000003E80000006400000001660688CE00000044000000000000000000000000000000000000003200000000uyuni-tools/uyuni-tools.changes.cbosdonnat.mounts- Add mgradm install podman arguments to define big volumes storage
07070100000120000081A4000003E80000006400000001660688CE00000015000000000000000000000000000000000000002C00000000uyuni-tools/uyuni-tools.changes.mbussolotto- Fix --version flag
07070100000121000081A4000003E80000006400000001660688CE00000022000000000000000000000000000000000000003E00000000uyuni-tools/uyuni-tools.changes.mbussolotto.changelog_upgrade- add inspect and upgrade command
07070100000122000081A4000003E80000006400000001660688CE0000002D000000000000000000000000000000000000003F00000000uyuni-tools/uyuni-tools.changes.mbussolotto.changelog_workflow- add github workflow for checking changelog
07070100000123000081A4000003E80000006400000001660688CE0000002B000000000000000000000000000000000000004400000000uyuni-tools/uyuni-tools.changes.mbussolotto.deny_uyuni_suma_upgrade- Deny uyuni to suma upgrade and viceversa
07070100000124000081A4000003E80000006400000001660688CE0000002E000000000000000000000000000000000000003E00000000uyuni-tools/uyuni-tools.changes.mbussolotto.fix_k3s_migration- k8s migration use same functions as upgrade
07070100000125000081A4000003E80000006400000001660688CE00000033000000000000000000000000000000000000003800000000uyuni-tools/uyuni-tools.changes.mbussolotto.fix_network- Improve error handling when exec.Command is used
07070100000126000081A4000003E80000006400000001660688CE0000002E000000000000000000000000000000000000004300000000uyuni-tools/uyuni-tools.changes.mbussolotto.image_tag_installation- allow installation using --image image:tag 
07070100000127000081A4000003E80000006400000001660688CE00000020000000000000000000000000000000000000003300000000uyuni-tools/uyuni-tools.changes.mbussolotto.mgrctl- ignore error on optional flag
07070100000128000081A4000003E80000006400000001660688CE00000029000000000000000000000000000000000000003900000000uyuni-tools/uyuni-tools.changes.mbussolotto.registration- pull image from authenticated registry
07070100000129000081A4000003E80000006400000001660688CE0000002A000000000000000000000000000000000000003A00000000uyuni-tools/uyuni-tools.changes.mbussolotto.registration2- allow to use images from RPM if present
0707010000012A000081A4000003E80000006400000001660688CE00000052000000000000000000000000000000000000004200000000uyuni-tools/uyuni-tools.changes.mbussolotto.set_cobbler_localhost- refactor upgrade to clarify script end adding post upgrade script (bsc#1219887)
0707010000012B000081A4000003E80000006400000001660688CE0000002D000000000000000000000000000000000000004200000000uyuni-tools/uyuni-tools.changes.mbussolotto.start_stop_kubernetes- Start/Stop/Restart command with kubernetes
0707010000012C000081A4000003E80000006400000001660688CE00000033000000000000000000000000000000000000003300000000uyuni-tools/uyuni-tools.changes.nadvornik.register- Add command to register Peripheral server to Hub
0707010000012D000081A4000003E80000006400000001660688CE0000002C000000000000000000000000000000000000002D00000000uyuni-tools/uyuni-tools.changes.nodeg.mgradm- Fix minimal administrator password length
0707010000012E000081A4000003E80000006400000001660688CE0000004E000000000000000000000000000000000000003E00000000uyuni-tools/uyuni-tools.changes.oholecek.fix_http_portpublish- Port 80 should be published to the port 80 of the containers. 8080 is squid
0707010000012F000081A4000003E80000006400000001660688CE00000039000000000000000000000000000000000000004400000000uyuni-tools/uyuni-tools.changes.oholecek.obsolete_uyuni_systemd_okg- Obsolete uyuni-proxy-systemd-service package by mgrpxy
07070100000130000081A4000003E80000006400000001660688CE00000044000000000000000000000000000000000000003B00000000uyuni-tools/uyuni-tools.changes.rmateus.update_system_list- Schedule a system list refresh after migrate if not runned before
07070100000131000081A4000003E80000006400000001660688CE00000057000000000000000000000000000000000000003700000000uyuni-tools/uyuni-tools.changes.witek.monitoring_ports- Add Node exporter (9100) and Taskomatic (9800) ports to the list
  of open TCP ports
07070100000132000081A4000003E80000006400000001660688CE00002D03000000000000000000000000000000000000001D00000000uyuni-tools/uyuni-tools.spec#
# spec file for package uyuni-tools
#
# Copyright (c) 2024 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
# upon. The license for this file, and modifications and additions to the
# file, is the same license as for the pristine package itself (unless the
# license for the pristine package is not an Open Source License, in which
# case the license is the MIT License). An "Open Source License" is a
# license that conforms to the Open Source Definition (Version 1.9)
# published by the Open Source Initiative.

# Please submit bugfixes or comments via https://bugs.opensuse.org/
#


%global provider        github
%global provider_tld    com
%global org             uyuni-project
%global project         uyuni-tools
%global provider_prefix %{provider}.%{provider_tld}/%{org}/%{project}
%global productname     Uyuni

%global namespace       registry.opensuse.org/uyuni

%if 0%{?suse_version} >= 1600 || 0%{?sle_version} >= 150400 || 0%{?rhel} >= 8 || 0%{?fedora} >= 37 || 0%{?debian} >= 12 || 0%{?ubuntu} >= 2004
%define adm_build    1
%else
%define adm_build    0
%endif

%define name_adm mgradm
%define name_ctl mgrctl
%define name_pxy mgrpxy

# Completion files
%if 0%{?debian} || 0%{?ubuntu}
%define _zshdir %{_datarootdir}/zsh/vendor-completions
%else
%define _zshdir %{_datarootdir}/zsh/site-functions
%endif

Name:           %{project}
Version:        0.1.5
Release:        0
Summary:        Tools for managing %{productname} container
License:        Apache-2.0
Group:          System/Management
URL:            https://%{provider_prefix}
Source0:        %{name}-%{version}.tar.gz
Source1:        vendor.tar.gz
BuildRequires:  bash-completion
BuildRequires:  coreutils
%if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu}
BuildRequires:  fish
%endif
BuildRequires:  zsh
# Get the proper Go version on different distros
%if 0%{?suse_version}
BuildRequires:  golang(API) >= 1.20
%endif
%if 0%{?ubuntu}
%define go_version      1.20
BuildRequires:  golang-%{go_version}
%endif
%if 0%{?debian}
BuildRequires:  golang >= 1.20
%endif
%if 0%{?fedora} || 0%{?rhel}
BuildRequires:  golang >= 1.19
%endif

%description
Tools for managing uyuni container.


%if %{adm_build}

%package -n %{name_adm}
Summary:        Command line tool to install and update %{productname}
%if 0%{?suse_version}
Requires:       (aardvark-dns if netavark)
%endif

%description -n %{name_adm}
%{name_adm} is a convenient tool to install and update %{productname} components as containers running
either on Podman or a Kubernetes cluster.

%package -n %{name_pxy}
Summary:        Command line tool to install and update %{productname} proxy
%if 0%{?suse_version}
Requires:       (aardvark-dns if netavark)
%endif
Obsoletes:      uyuni-proxy-systemd-services

%description -n %{name_pxy}
%{name_pxy} is a convenient tool to install and update %{productname} proxy components as containers
running either on Podman or a Kubernetes cluster.

%package -n %{name_adm}-bash-completion
Summary:        Bash Completion for %{name_adm}
Group:          System/Shells
Requires:       %{name_adm} = %{version}
%if 0%{?suse_version} >= 150000
Supplements:    (%{name_adm} and bash-completion)
%else
Supplements:    bash-completion
%endif
BuildArch:      noarch

%description -n %{name_adm}-bash-completion
Bash command line completion support for %{name_adm}.

%package -n %{name_adm}-zsh-completion
Summary:        Zsh Completion for %{name_adm}
Group:          System/Shells
Requires:       %{name_adm} = %{version}
%if 0%{?suse_version} >= 150000
Supplements:    (%{name_adm} and zsh)
%else
Supplements:    zsh
%endif
BuildArch:      noarch

%description -n %{name_adm}-zsh-completion
Zsh command line completion support for %{name_adm}.

%package -n %{name_pxy}-bash-completion
Summary:        Bash Completion for %{name_pxy}
Group:          System/Shells
Requires:       %{name_pxy} = %{version}
%if 0%{?suse_version} >= 150000
Supplements:    (%{name_pxy} and bash-completion)
%else
Supplements:    bash-completion
%endif
BuildArch:      noarch

%description -n %{name_pxy}-bash-completion
Bash command line completion support for %{name_pxy}.

%package -n %{name_pxy}-zsh-completion
Summary:        Zsh Completion for %{name_pxy}
Group:          System/Shells
Requires:       %{name_pxy} = %{version}
%if 0%{?suse_version} >= 150000
Supplements:    (%{name_pxy} and zsh)
%else
Supplements:    zsh
%endif
BuildArch:      noarch

%description -n %{name_pxy}-zsh-completion
Zsh command line completion support for %{name_pxy}.


%if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu}
%package -n %{name_adm}-fish-completion
Summary:        Fish Completion for %{name_adm}
Group:          System/Shells
Requires:       %{name_adm} = %{version}
%if 0%{?suse_version} >= 150000
Supplements:    (%{name_adm} and fish)
%else
Supplements:    fish
%endif
BuildArch:      noarch

%description -n %{name_adm}-fish-completion
Fish command line completion support for %{name_adm}.

%package -n %{name_pxy}-fish-completion
Summary:        Fish Completion for %{name_pxy}
Group:          System/Shells
Requires:       %{name_pxy} = %{version}
%if 0%{?suse_version} >= 150000
Supplements:    (%{name_pxy} and fish)
%else
Supplements:    fish
%endif
BuildArch:      noarch

%description -n %{name_pxy}-fish-completion
Fish command line completion support for %{name_pxy}.

%endif

%endif

%package -n %{name_ctl}
Summary:        Command line tool to perform day-to-day operations on %{productname}

%description -n %{name_ctl}
%{name_ctl} is a tool helping with dayly tasks on %{productname} components running as containers
either on Podman or a Kubernetes cluster.

%package -n %{name_ctl}-bash-completion
Summary:        Bash Completion for %{name_ctl}
Group:          System/Shells
Requires:       %{name_ctl} = %{version}
%if 0%{?suse_version} >= 150000
Supplements:    (%{name_ctl} and bash-completion)
%else
Supplements:    bash-completion
%endif
BuildArch:      noarch

%description -n %{name_ctl}-bash-completion
Bash command line completion support for %{name_ctl}.

%package -n %{name_ctl}-zsh-completion
Summary:        Zsh Completion for %{name_ctl}
Group:          System/Shells
Requires:       %{name_ctl} = %{version}
%if 0%{?suse_version} >= 150000
Supplements:    (%{name_ctl} and zsh)
%else
Supplements:    zsh
%endif
BuildArch:      noarch

%description -n %{name_ctl}-zsh-completion
Zsh command line completion support for %{name_ctl}.


%if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu}
%package -n %{name_ctl}-fish-completion
Summary:        Fish Completion for %{name_ctl}
Group:          System/Shells
Requires:       %{name_ctl} = %{version}
%if 0%{?suse_version} >= 150000
Supplements:    (%{name_ctl} and fish)
%else
Supplements:    fish
%endif
BuildArch:      noarch

%description -n %{name_ctl}-fish-completion
Fish command line completion support for %{name_ctl}.
%endif

%lang_package -n %{name_ctl}

%prep
%autosetup
tar -zxf %{SOURCE1}

%build
export GOFLAGS=-mod=vendor
mkdir -p bin
UTILS_PATH="%{provider_prefix}/shared/utils"

tag=%{!?_default_tag:latest}
%if "%{?_default_tag}" != ""
    tag='%{_default_tag}'
%endif

image=%{namespace}
%if "%{?_default_namespace}" != ""
  namespace='%{_default_namespace}'
%endif

go_tags=""
%if "%{?_uyuni_tools_tags}" != ""
  go_tags="-tags %{_uyuni_tools_tags}"
%endif

go_path=
%if 0%{?ubuntu}
  go_path=/usr/lib/go-%{go_version}/bin/
%else
  %if "%{?_go_bin}" != ""
    go_path='%{_go_bin}/'
  %endif
%endif

GOLD_FLAGS="-X ${UTILS_PATH}.Version=%{version} -X ${UTILS_PATH}.LocaleRoot=%{_datadir}/locale"
if test -n "${namespace}"; then
    GOLD_FLAGS="${GOLD_FLAGS} -X ${UTILS_PATH}.DefaultNamespace=${namespace} -X ${UTILS_PATH}.DefaultTag=${tag}"
fi

if test -n "${tag}"; then
    GOLD_FLAGS="${GOLD_FLAGS} -X ${UTILS_PATH}.DefaultTag=${tag}"
fi

# Workaround for rpm on Fedora and EL clones not able to handle go's compressed debug symbols
# Found compressed .debug_aranges section, not attempting dwz compression
%if 0%{?rhel} >= 8 || 0%{?fedora} >= 38
GOLD_FLAGS="-compressdwarf=false ${GOLD_FLAGS}"
%endif

# Workaround for missing build-id on Fedora
# error: Missing build-id in [...]
%if 0%{?fedora} >= 38
GOLD_FLAGS="-B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \n') ${GOLD_FLAGS}"
%endif

${go_path}go build ${go_tags} -ldflags "${GOLD_FLAGS}" -o ./bin ./...

%if ! %{adm_build}
rm ./bin/%{name_adm}
rm ./bin/%{name_pxy}
%endif

%install
install -m 0755 -vd %{buildroot}%{_bindir}
install -m 0755 -vp ./bin/* %{buildroot}%{_bindir}/

# Generate the machine object files for localizations
pushd locale
./build.sh %{buildroot}%{_datadir}/locale/
popd

%find_lang %{name_ctl} %{?no_lang_C}

# Completion files
mkdir -p %{buildroot}%{_datarootdir}/bash-completion/completions/
mkdir -p %{buildroot}%{_zshdir}

%{buildroot}/%{_bindir}/%{name_ctl} completion bash > %{buildroot}%{_datarootdir}/bash-completion/completions/%{name_ctl}
%{buildroot}/%{_bindir}/%{name_ctl} completion zsh > %{buildroot}%{_zshdir}/_%{name_ctl}

%if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu}
mkdir -p %{buildroot}%{_datarootdir}/fish/vendor_completions.d/
%{buildroot}/%{_bindir}/%{name_ctl} completion fish > %{buildroot}%{_datarootdir}/fish/vendor_completions.d/%{name_ctl}.fish
%endif

%if %{adm_build}

%{buildroot}/%{_bindir}/%{name_adm} completion bash > %{buildroot}%{_datarootdir}/bash-completion/completions/%{name_adm}
%{buildroot}/%{_bindir}/%{name_adm} completion zsh > %{buildroot}%{_zshdir}/_%{name_adm}

%{buildroot}/%{_bindir}/%{name_pxy} completion bash > %{buildroot}%{_datarootdir}/bash-completion/completions/%{name_pxy}
%{buildroot}/%{_bindir}/%{name_pxy} completion zsh > %{buildroot}%{_zshdir}/_%{name_pxy}

%if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu}
%{buildroot}/%{_bindir}/%{name_adm} completion fish > %{buildroot}%{_datarootdir}/fish/vendor_completions.d/%{name_adm}.fish
%{buildroot}/%{_bindir}/%{name_pxy} completion fish > %{buildroot}%{_datarootdir}/fish/vendor_completions.d/%{name_pxy}.fish
%endif

%endif

%if %{adm_build}

%files -n %{name_adm}
%defattr(-,root,root)
%doc README.md
%license LICENSE
%{_bindir}/%{name_adm}

%files -n %{name_adm}-bash-completion
%{_datarootdir}/bash-completion/completions/%{name_adm}

%files -n %{name_adm}-zsh-completion
%{_zshdir}/_%{name_adm}

%if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu}
%files -n %{name_adm}-fish-completion
%{_datarootdir}/fish/vendor_completions.d/%{name_adm}.fish
%endif

%files -n %{name_pxy}
%defattr(-,root,root)
%doc README.md
%license LICENSE
%{_bindir}/%{name_pxy}

%files -n %{name_pxy}-bash-completion
%{_datarootdir}/bash-completion/completions/%{name_pxy}

%files -n %{name_pxy}-zsh-completion
%{_zshdir}/_%{name_pxy}

%if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu}
%files -n %{name_pxy}-fish-completion
%{_datarootdir}/fish/vendor_completions.d/%{name_pxy}.fish
%endif

%endif

%files -n %{name_ctl}
%defattr(-,root,root)
%doc README.md
%license LICENSE
%{_bindir}/%{name_ctl}

%files -n %{name_ctl}-bash-completion
%{_datarootdir}/bash-completion/completions/%{name_ctl}

%files -n %{name_ctl}-zsh-completion
%{_zshdir}/_%{name_ctl}

%if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu}
%files -n %{name_ctl}-fish-completion
%{_datarootdir}/fish/vendor_completions.d/%{name_ctl}.fish
%endif

%files -n %{name_ctl}-lang -f %{name_ctl}.lang

%changelog
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!
openSUSE Build Service is sponsored by