diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 3fc9400f5..000000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,152 +0,0 @@ - -name: CI -on: - push: - branches: - - develop - - release* - - tags: [v*] - paths-ignore: - - README.md - - CHANGELOG.md - - LICENSE - pull_request: - # Sequence of patterns matched against refs/heads - branches: - - develop - - release* - - paths-ignore: - - README.md - - CHANGELOG.md - - LICENSE -env: - PROJECT: 'iofog' - controller_image: 'ghcr.io/eclipse-iofog/controller:latest' - agent_image: 'ghcr.io/eclipse-iofog/agent:latest' - operator_image: 'ghcr.io/eclipse-iofog/operator:latest' - port_manager_image: 'ghcr.io/eclipse-iofog/port-manager:latest' - router_image: 'ghcr.io/eclipse-iofog/router:latest' - router_arm_image: 'ghcr.io/eclipse-iofog/router:latest' - # proxy_image: 'ghcr.io/eclipse-iofog/proxy:latest' - # proxy_arm_image: 'ghcr.io/eclipse-iofog/proxy:latest' - iofog_agent_version: '3.5.0' - controller_version: '3.5.0' - version: - agent_vm_list: - controller_vm: - -jobs: - Build: - runs-on: ubuntu-22.04 - permissions: - contents: 'read' - id-token: 'write' - packages: 'write' - name: Build - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - # - name: Install system dependencies - # run: | - # sudo apt-get update - # sudo apt-get install -y libgpgme-dev - - name: Install ARM cross-compilation toolchain - run: | - sudo apt-get update - sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf - - uses: actions/setup-go@v4 - with: - go-version: '1.24' - cache: false - - run: go version - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: v1.64.4 - args: --timeout=5m0s - working-directory: . - - name: Run bootstrap - run: PIPELINE=1 script/bootstrap.sh - - name: 'Get Previous tag' - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" - with: - fallback: v0.0.0 - - name: Set image tag - shell: bash - id: tags - run: | - if [[ ${{ github.ref_name }} =~ ^v.* ]] ; then - echo "VERSION=${{ github.ref_name }}" >> "${GITHUB_OUTPUT}" - else - echo "VERSION=${{ steps.previoustag.outputs.tag }}-dev" >> "${GITHUB_OUTPUT}" - fi - - name: Get image tag - run: | - echo ${{ steps.tags.outputs.VERSION }} - - name: iofogctl build packages GoReleaser - uses: goreleaser/goreleaser-action@v6 - with: - version: '~> v2' - args: --snapshot --clean --verbose --config ./.goreleaser-iofogctl.yml - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: iofogctl - path: ${{ github.workspace }}/dist - - Publish_iofogctl_Prod: - needs: [Build] - - runs-on: ubuntu-latest - permissions: - actions: write - checks: write - contents: write - deployments: write - id-token: write - issues: write - discussions: write - packages: write - pages: write - pull-requests: write - repository-projects: write - security-events: write - statuses: write - name: Publish iofogctl Prod - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - # - name: Install system dependencies - # run: | - # sudo apt-get update - # sudo apt-get install -y libgpgme-dev - - name: Install ARM cross-compilation toolchain - run: | - sudo apt-get update - sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf - - uses: actions/setup-go@v4 - with: - go-version: '1.23.0' - cache: false - - name: Get image tag - run: | - echo ${{ needs.Build.outputs.VERSION }} - - name: iofogctl build packages GoReleaser - uses: goreleaser/goreleaser-action@v6 - with: - version: '~> v2' - args: --clean --verbose --config ./.goreleaser-iofogctl.yml - env: - VERSION: ${{ needs.Build.outputs.VERSION }} - GITHUB_TOKEN: ${{ secrets.PAT }} - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: iofogctl - path: ${{ github.workspace }}/dist - overwrite: true \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..fc1f9ef25 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,152 @@ +name: CLI CI + +on: + push: + branches: [develop, release*] + tags: ['v*'] + paths-ignore: + - README.md + - CHANGELOG.md + - LICENSE + pull_request: + branches: [develop, release*] + paths-ignore: + - README.md + - CHANGELOG.md + - LICENSE + workflow_dispatch: + +permissions: read-all + +env: + GO_VERSION: '1.26.4' + +jobs: + grep-gates: + name: Grep gates + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Forbidden flavor strings + run: make grep-gates + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + - run: go version + - name: golangci-lint + uses: golangci/golangci-lint-action@db582008a42febd596419635a5abc9d9815daa9c # v9.2.1 + with: + version: v2.12.2 + args: --timeout=5m0s + + security: + name: Security + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + - name: gosec + run: make security-code + - name: govulncheck + run: make vulncheck + + unit-iofog: + name: Unit (iofog) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + - name: Unit tests + run: make FLAVOR=iofog test-unit + + unit-datasance: + name: Unit (datasance) + if: github.repository == 'Datasance/potctl' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + - name: Unit tests + run: make FLAVOR=datasance test-unit + + smoke-iofog: + name: Smoke (iofogctl) + needs: [lint, grep-gates, unit-iofog] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + - name: Smoke + run: make FLAVOR=iofog smoke + + smoke-datasance: + name: Smoke (potctl) + if: github.repository == 'Datasance/potctl' + needs: [lint, grep-gates, unit-datasance] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + - name: Smoke + run: make FLAVOR=datasance BINARY_NAME=potctl PACKAGE_DIR=cmd/potctl smoke + + build-iofogctl: + name: Build iofogctl + needs: [smoke-iofog] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + - name: Verify embed assets + run: test -f assets/embed.go + - name: Build + run: make iofogctl + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: iofogctl-linux-amd64 + path: bin/iofogctl + + build-potctl: + name: Build potctl + if: github.repository == 'Datasance/potctl' + needs: [smoke-datasance] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + - name: Verify embed assets + run: test -f assets/embed.go + - name: Build + run: make potctl + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: potctl-linux-amd64 + path: bin/potctl diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..368bb44d4 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,34 @@ +name: CodeQL + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + +permissions: read-all + +env: + GO_VERSION: '1.26.4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3 + with: + languages: go + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + - uses: github/codeql-action/autobuild@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3 + - uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3 + with: + category: "/language:go" diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml new file mode 100644 index 000000000..1275eabed --- /dev/null +++ b/.github/workflows/govulncheck.yml @@ -0,0 +1,39 @@ +name: govulncheck + +on: + push: + paths: + - go.sum + schedule: + - cron: "0 0 * * *" + workflow_dispatch: {} + +permissions: read-all + +env: + GO_VERSION: '1.26.4' + +jobs: + govulncheck: + name: govulncheck + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 + + - name: Run govulncheck + run: | + chmod +x scripts/vulncheck.sh + scripts/vulncheck.sh + + - name: Verify module integrity + run: go mod verify diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..59802b4d2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +on: + push: + tags: ['v*'] + workflow_dispatch: {} + +env: + GO_VERSION: '1.26.4' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + + - name: Install cross-compilers + run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf + + - name: Install goreleaser + run: go install github.com/goreleaser/goreleaser/v2@latest + + - name: Release + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + run: script/goreleaser-release.sh release --clean diff --git a/.gitignore b/.gitignore index c1e3f1041..dcb13fa96 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ test-report.xml dist/ vendor/ bats-core/ -.DS_Store \ No newline at end of file +.DS_Store + +.cursor \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml index 26e29d5a4..66dbb7b8f 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,235 +1,141 @@ +# golangci-lint v2 — Go 1.26.4, module github.com/eclipse-iofog/iofogctl +# +# Layers: +# 1. edgelet/.golangci.yaml parity (govet, revive, staticcheck, errcheck, misspell, errorlint) +# 2. iofogctl addons: depguard import allowlist + golang.org/x/* + gopkg.in/yaml.v2 +# 3. Legacy monolith exemptions (revive rules + ST1003) — re-enable incrementally -linters: - # please, do not use `enable-all`: it's deprecated and will be removed soon. - # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint - disable-all: true - enable: -# - asasalint - - asciicheck -# - bidichk - # - bodyclose -# - contextcheck - # - cyclop -# - decorder - - depguard - - dogsled -# - durationcheck - # - errcheck - # - errchkjson -# - errname -# - execinquery -# - exportloopref - # - forbidigo - # - forcetypeassert - - funlen - # - revive - - typecheck - # - dupl - # - dupword - # - errorlint - # - exhaustruct - # - gochecknoglobals - # - gochecknoinits - # - gocognit - - goconst - # - gocritic # probably should re-enable this - - gocyclo - # - godot - # - godox # disabling because we have WAY too many TODOs etc. - # - goerr113 # TODO: reenable - # - gofmt - # - gofumpt # not using this - - goheader -# - goimports -# - gomnd -# - gomoddirectives -# - gomodguard -# - goprintffuncname -# - gosec -# - gosimple -# - govet -# - grouper -# - ifshort -# - importas -# - ineffassign -# - interfacebloat -# - interfacer -# - ireturn -# - lll -# - loggercheck -# - maintidx -# - makezero -# - maligned -# - misspell -# - nakedret -# - nestif -# - nilerr -# - nilnil -# - nlreturn -# - noctx -# - nolintlint -# - nonamedreturns -# - nosnakecase -# - nosprintfhostport -# - paralleltest -# - prealloc -# - predeclared -# - promlinter -# - reassign -# - rowserrcheck -# - scopelint -# - sqlclosecheck -# - staticcheck -# - structcheck -# - stylecheck -# - tagliatelle -# - tenv -# - testableexamples -# - testpackage -# - thelper -# - tparallel -# - unconvert -# - unparam -# - unused -# - usestdlibvars -# - varcheck -# - varnamelen -# - wastedassign -# - whitespace -# - wrapcheck -# - wsl - - -linters-settings: - cyclop: - max-complexity: 30 - skip-tests: true - depguard: - rules: - main: - list-mode: strict - files: - - $all - - "!$test" - allow: - - $gostd - - github.com/eclipse-iofog/iofogctl - - github.com/eclipse-iofog/iofog-go-sdk/v3 - - github.com/eclipse-iofog/iofog-operator - - github.com/spf13/cobra - - github.com/mitchellh/go-homedir - - github.com/pkg - - github.com/twmb/algoimpl - - github.com/containers/image - - github.com/opencontainers/go-digest - - github.com/gorilla/websocket - - github.com/vmihailenco/msgpack - - github.com/docker - - k8s.io - - sigs.k8s.io/controller-runtime - - github.com/GeertJohan/go.rice - - github.com/briandowns/spinner - dupl: - threshold: 100 - funlen: - lines: 250 - statements: 100 - goconst: - min-len: 2 - min-occurrences: 5 - gocritic: - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - disabled-checks: - - dupImport # https://github.com/go-critic/go-critic/issues/845 - - ifElseChain - - octalLiteral - - whyNoLint - - wrapperFunc - gocognit: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 30 - gocyclo: - min-complexity: 30 - godox: - # Report any comments starting with keywords, this is useful for TODO or FIXME comments that - # might be left in the code accidentally and should be resolved before merging. - # Default: ["TODO", "BUG", "FIXME"] - keywords: - - TODO - - BUG - - FIXME - gofmt: - # Simplify code: gofmt with `-s` option. - # Default: true - simplify: true - # Apply the rewrite rules to the source before reformatting. - # https://pkg.go.dev/cmd/gofmt - # Default: [] - rewrite-rules: - - pattern: 'interface{}' - replacement: 'any' - - pattern: 'a[b:len(a)]' - replacement: 'a[b:]' - goimports: - # local-prefixes: github.com/golangci/golangci-lint - gomnd: - settings: - mnd: - # don't include the "operation" and "assign" - checks: argument,case,condition,return - govet: - check-shadowing: false - settings: - printf: - funcs: - lll: - line-length: 180 - maligned: - suggest-new: true - misspell: - locale: US - nestif: - min-complexity: 10 - - varnamelen: - # The longest distance, in source lines, that is being considered a "small scope". - # Variables used in at most this many lines will be ignored. - # Default: 5 - max-distance: 50 - # The minimum length of a variable's name that is considered "long". - # Variable names that are at least this long will be ignored. - # Default: 3 - min-name-length: 2 +version: "2" issues: - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - - path: _test\.go - linters: - - gomnd - - lll - - maligned - - gocyclo - - dupl - - funlen - -run: - skip-files: - # auto-generated - - ".*_test.go" - - + max-issues-per-linter: 0 + max-same-issues: 0 +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - ^vendor/ -# golangci.com configuration -# https://github.com/golangci/golangci/wiki/Configuration -service: - golangci-lint-version: 1.64.4 # use the fixed version to not introduce new linters unexpectedly - prepare: - - echo "here I can run custom commands, but no preparation needed for this repo" +linters: + default: none + enable: + - govet + - revive + - staticcheck + - errcheck + - misspell + - errorlint + - depguard + settings: + revive: + enable-all-rules: true + rules: + - {name: add-constant, disabled: true} + - {name: argument-limit, disabled: true} + - {name: cognitive-complexity, disabled: true} + - {name: confusing-naming, disabled: true} + - {name: confusing-results, disabled: true} + - {name: cyclomatic, disabled: true} + - {name: early-return, disabled: true} + - {name: empty-block, disabled: true} + - {name: enforce-switch-style, disabled: true} + - {name: flag-parameter, disabled: true} + - {name: function-length, disabled: true} + - {name: function-result-limit, disabled: true} + - {name: import-shadowing, disabled: true} + - {name: line-length-limit, disabled: true} + - {name: max-control-nesting, disabled: true} + - {name: max-public-structs, disabled: true} + - {name: redundant-import-alias, disabled: true} + - {name: unsecure-url-scheme, disabled: true} + - {name: unused-parameter, disabled: true} + - {name: unused-receiver, disabled: true} + - {name: use-waitgroup-go, disabled: true} + - {name: var-naming, disabled: true} + # iofogctl legacy monolith — re-enable in hygiene sprint + - {name: bare-return, disabled: true} + - {name: package-directory-mismatch, disabled: true} + - {name: if-return, disabled: true} + - {name: unhandled-error, disabled: true} + - {name: use-fmt-print, disabled: true} + - {name: deep-exit, disabled: true} + - {name: unexported-naming, disabled: true} + - {name: use-any, disabled: true} + - {name: empty-lines, disabled: true} + - {name: comment-spacings, disabled: true} + - {name: unnecessary-stmt, disabled: true} + - {name: useless-break, disabled: true} + - {name: use-slices-sort, disabled: true} + - {name: identical-switch-branches, disabled: true} + - {name: indent-error-flow, disabled: true} + - {name: identical-ifelseif-branches, disabled: true} + - {name: use-errors-new, disabled: true} + - {name: struct-tag, disabled: true} + - {name: get-return, disabled: true} + - {name: unchecked-type-assertion, disabled: true} + - {name: unnecessary-format, disabled: true} + - {name: identical-branches, disabled: true} + - {name: datarace, disabled: true} + - {name: package-naming, disabled: true} + - {name: import-alias-naming, disabled: true} + - {name: defer, disabled: true} + - {name: var-declaration, disabled: true} + - {name: unreachable-code, disabled: true} + - {name: redundant-build-tag, disabled: true} + - {name: redefines-builtin-id, disabled: true} + staticcheck: + checks: + - all + - -SA1019 + - -ST1003 + misspell: + locale: US + errorlint: + errorf: true + asserts: true + comparison: true + depguard: + rules: + main: + list-mode: strict + files: + - $all + - "!$test" + allow: + - $gostd + - github.com/eclipse-iofog/iofogctl + - github.com/eclipse-iofog/iofog-go-sdk/v3 + - github.com/eclipse-iofog/iofog-operator + - github.com/spf13/cobra + - github.com/mitchellh/go-homedir + - github.com/pkg + - github.com/twmb/algoimpl + - go.podman.io/image + - github.com/opencontainers/go-digest + - github.com/gorilla/websocket + - github.com/vmihailenco/msgpack + - github.com/moby/moby + - k8s.io + - sigs.k8s.io/controller-runtime + - github.com/briandowns/spinner + - gopkg.in/yaml.v2 + - golang.org/x/crypto + - golang.org/x/oauth2 + - golang.org/x/sys + - golang.org/x/term + exclusions: + generated: lax + paths: + - ^vendor/ + presets: + - comments + - common-false-positives + - legacy + - std-error-handling +run: + timeout: 5m diff --git a/.goreleaser-iofogctl-dev.yml b/.goreleaser-iofogctl-dev.yml deleted file mode 100644 index 269adffd2..000000000 --- a/.goreleaser-iofogctl-dev.yml +++ /dev/null @@ -1,199 +0,0 @@ -# goreleaser config for iofogctl. See: https://goreleaser.com -# -# To execute goreleaser, use the mage targets: -# -# $ mage iofogctl:snapshot -# $ mage iofogctl:release -# -# The snapshot target builds the installation packages (brew, rpm, -# deb, etc), into the dist dir. -# The release target does the same, but also publishes the packages. -# -# See README.md for more. -version: 2 -project_name: iofogctl -env: - - GO111MODULE=on - - CGO_ENABLED=0 -before: - hooks: - - go version - -builds: - - id: build_macos - binary: iofogctl - env: - main: ./cmd/iofogctl/main.go - goos: - - darwin - goarch: - - amd64 - - arm64 - ldflags: - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.versionNumber=v{{.Version}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.commit={{ .ShortCommit }}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.date={{.Date}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.platform={{.Os}}/{{.Arch}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.operatorTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.routerTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerVersion=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentVersion=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.debuggerTag=latest" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.natsTag=3.6.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.repo=ghcr.io/eclipse-iofog" - - - id: build_linux - binary: iofogctl - main: ./cmd/iofogctl/ - goos: - - linux - goarch: - - amd64 - - arm64 - - arm - goarm: - - 6 - - 7 - ldflags: - - -extldflags -static - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.versionNumber=v{{.Version}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.commit={{ .ShortCommit }}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.date={{.Date}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.platform={{.Os}}/{{.Arch}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.operatorTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.routerTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerVersion=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentVersion=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.debuggerTag=latest" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.natsTag=3.6.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.repo=ghcr.io/eclipse-iofog" - flags: - - -v - - - id: build_windows - binary: iofogctl - env: - main: ./cmd/iofogctl/main.go - goos: - - windows - goarch: - - amd64 - ldflags: - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.versionNumber=v{{.Version}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.commit={{ .ShortCommit }}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.date={{.Date}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.platform={{.Os}}/{{.Arch}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.operatorTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.routerTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerVersion=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentVersion=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.debuggerTag=latest" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.natsTag=3.6.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.repo=ghcr.io/eclipse-iofog" - - -archives: - - - id: linux - ids: - - build_linux - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - formats: [ 'tar.gz' ] - files: - - README.md - - LICENSE - - - id: macos - ids: - - build_macos - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ if eq .Os `darwin` }}macos{{ else }}{{ .Os }}{{ end }}_{{ .Arch }}" - formats: [ 'tar.gz' ] - files: - - README.md - - LICENSE - -checksum: - name_template: "{{.ProjectName}}-checksums.txt" - -snapshot: - version_template: "{{ .Version }}~dev" - -changelog: - disable: true - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' - - '^dev:' - - 'README' - - Merge pull request - - Merge branch - - -release: - github: - owner: eclipse-iofog - name: iofogctl - - # If set to true, will not auto-publish the release. Default is false. - draft: false - - # If set to auto, will mark the release as not ready for production - # in case there is an indicator for this in the tag e.g. v1.0.0-rc1 - # If set to true, will mark the release as not ready for production. - # Default is false. - prerelease: auto - -brews: - - - name: iofogctl - homepage: "https://github.com/eclipse-iofog/iofogctl" - description: "CLI for ioFog" - - repository: - owner: eclipse-iofog - name: homebrew-iofogctl - - url_template: "https://github.com/eclipse-iofog/iofogctl/releases/download/{{ .Tag }}/{{ .ArtifactName }}" - - commit_author: - name: emirhandurmus - email: emirhan.durmus@datasance.com - - directory: Formula - - test: | - system "#{bin}/iofogctl version" - install: | - bin.install "iofogctl" - skip_upload: false - -nfpms: - - - ids: ['build_linux'] - homepage: "https://github.com/eclipse-iofog/iofogctl" - description: CLI for ioFog - maintainer: Contributors to the Eclipse ioFog Project - vendor: Datasance - - - formats: - - deb - - rpm - - apk - - overrides: - deb: - file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - rpm: - # Note: file_name_template must have this EXACT format - file_name_template: "{{ .ProjectName }}-{{ .Version }}-1.{{ if eq .Arch `amd64` }}x86_64{{ else if eq .Arch `arm64` }}aarch64{{ else }}{{ .Arch }}{{ end }}{{ if .Arm }}v{{ .Arm }}hl{{ end }}" - apk: - file_name_template: "{{ .ConventionalFileName }}" \ No newline at end of file diff --git a/.goreleaser-iofogctl.yml b/.goreleaser-iofogctl.yml deleted file mode 100644 index 5203f0d0c..000000000 --- a/.goreleaser-iofogctl.yml +++ /dev/null @@ -1,263 +0,0 @@ -# goreleaser config for iofogctl. See: https://goreleaser.com -# -# To execute goreleaser, use the mage targets: -# -# $ mage iofogctl:snapshot -# $ mage iofogctl:release -# -# The snapshot target builds the installation packages (brew, rpm, -# deb, etc), into the dist dir. -# The release target does the same, but also publishes the packages. -# -# See README.md for more. -version: 2 -project_name: iofogctl -env: - - GO111MODULE=on - - CGO_ENABLED=1 -before: - hooks: - - go version - -builds: - - id: build_macos - binary: iofogctl - env: - - CGO_ENABLED=0 - main: ./cmd/iofogctl/main.go - goos: - - darwin - goarch: - - amd64 - - arm64 - flags: - - -tags=containers_image_openpgp,exclude_graphdriver_btrfs - ldflags: - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.versionNumber=v{{.Version}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.commit={{ .ShortCommit }}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.date={{.Date}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.platform={{.Os}}/{{.Arch}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.operatorTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.routerTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerVersion=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentVersion=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.debuggerTag=latest" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.natsTag=3.6.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.repo=ghcr.io/eclipse-iofog" - - - id: build_linux_amd64 - binary: iofogctl - main: ./cmd/iofogctl/ - goos: - - linux - goarch: - - amd64 - flags: - - -v - - -tags=containers_image_openpgp,exclude_graphdriver_btrfs - ldflags: - - -extldflags -static - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.versionNumber=v{{.Version}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.commit={{ .ShortCommit }}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.date={{.Date}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.platform={{.Os}}/{{.Arch}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.operatorTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.routerTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerVersion=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentVersion=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.natsTag=3.6.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.repo=ghcr.io/eclipse-iofog" - - - id: build_linux_arm64 - binary: iofogctl - main: ./cmd/iofogctl/ - goos: - - linux - goarch: - - arm64 - env: - - CC=aarch64-linux-gnu-gcc - - CXX=aarch64-linux-gnu-g++ - - CGO_ENABLED=1 - flags: - - -v - - -tags=containers_image_openpgp,exclude_graphdriver_btrfs - ldflags: - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.versionNumber=v{{.Version}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.commit={{ .ShortCommit }}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.date={{.Date}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.platform={{.Os}}/{{.Arch}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.operatorTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.routerTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerVersion=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentVersion=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.natsTag=3.6.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.repo=ghcr.io/eclipse-iofog" - - - id: build_linux_arm - binary: iofogctl - main: ./cmd/iofogctl/ - goos: - - linux - goarch: - - arm - goarm: - - 6 - - 7 - env: - - CC=arm-linux-gnueabihf-gcc - - CXX=arm-linux-gnueabihf-g++ - - CGO_ENABLED=1 - flags: - - -v - - -tags=containers_image_openpgp,exclude_graphdriver_btrfs - ldflags: - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.versionNumber=v{{.Version}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.commit={{ .ShortCommit }}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.date={{.Date}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.platform={{.Os}}/{{.Arch}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.operatorTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.routerTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerVersion=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentVersion=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.debuggerTag=latest" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.natsTag=3.6.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.repo=ghcr.io/eclipse-iofog" - - - id: build_windows - binary: iofogctl - env: - - CGO_ENABLED=0 - main: ./cmd/iofogctl/main.go - goos: - - windows - goarch: - - amd64 - flags: - - -tags=containers_image_openpgp,exclude_graphdriver_btrfs - ldflags: - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.versionNumber=v{{.Version}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.commit={{ .ShortCommit }}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.date={{.Date}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.platform={{.Os}}/{{.Arch}}" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.operatorTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.routerTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerTag=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentTag=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerVersion=3.7.1" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.agentVersion=3.7.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.debuggerTag=latest" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.natsTag=3.6.0" - - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.repo=ghcr.io/eclipse-iofog" - - -archives: - - - id: linux - ids: - - build_linux_amd64 - - build_linux_arm64 - - build_linux_arm - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - formats: [ 'tar.gz' ] - files: - - README.md - - LICENSE - - - id: macos - ids: - - build_macos - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ if eq .Os `darwin` }}macos{{ else }}{{ .Os }}{{ end }}_{{ .Arch }}" - formats: [ 'tar.gz' ] - files: - - README.md - - LICENSE - -checksum: - name_template: "{{.ProjectName}}-checksums.txt" - -snapshot: - version_template: "{{ .Version }}-SNAPSHOT" - -changelog: - disable: true - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' - - '^dev:' - - 'README' - - Merge pull request - - Merge branch - - -release: - github: - owner: eclipse-iofog - name: iofogctl - - # If set to true, will not auto-publish the release. Default is false. - draft: false - - # If set to auto, will mark the release as not ready for production - # in case there is an indicator for this in the tag e.g. v1.0.0-rc1 - # If set to true, will mark the release as not ready for production. - # Default is false. - prerelease: auto - -brews: - - - name: iofogctl - homepage: "https://github.com/eclipse-iofog/iofogctl" - description: "CLI for ioFog" - goarm: 6 - - repository: - owner: eclipse-iofog - name: homebrew-iofogctl - - - url_template: "https://github.com/eclipse-iofog/iofogctl/releases/download/{{ .Tag }}/{{ .ArtifactName }}" - - commit_author: - name: emirhandurmus - email: emirhan.durmus@datasance.com - - directory: Formula - - test: | - system "#{bin}/iofogctl version" - install: | - bin.install "iofogctl" - skip_upload: false - -nfpms: - - - ids: ['build_linux_amd64', 'build_linux_arm64', 'build_linux_arm'] - homepage: "https://github.com/eclipse-iofog/iofogctl" - description: CLI for ioFog - maintainer: Contributors to the Eclipse ioFog Project - vendor: Datasance - - - formats: - - deb - - rpm - - apk - - overrides: - deb: - file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - rpm: - file_name_template: "{{ .ProjectName }}-{{ .Version }}-1.{{ if eq .Arch `amd64` }}x86_64{{ else if eq .Arch `arm64` }}aarch64{{ else }}{{ .Arch }}{{ end }}{{ if .Arm }}v{{ .Arm }}hl{{ end }}" - apk: - file_name_template: "{{ .ConventionalFileName }}" \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 000000000..5a1c5cfce --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,220 @@ +# Unified goreleaser config for potctl (datasance) and iofogctl (iofog). +# Flavor comes from GITHUB_REPOSITORY (or FLAVOR override); version pins from versions.mk: +# GITHUB_REPOSITORY=Datasance/potctl script/goreleaser-release.sh release --clean +# GITHUB_REPOSITORY=eclipse-iofog/iofogctl script/goreleaser-release.sh release --clean +version: 2 + +project_name: '{{ if eq (.Env.FLAVOR) "datasance" }}potctl{{ else }}iofogctl{{ end }}' + +env: + - GO111MODULE=on + - CGO_ENABLED=1 + +before: + hooks: + - go version + - bash script/goreleaser-env-check.sh + +builds: + - id: build_macos + binary: '{{ if eq (.Env.FLAVOR) "datasance" }}potctl{{ else }}iofogctl{{ end }}' + env: + - CGO_ENABLED=0 + main: '{{ if eq (.Env.FLAVOR) "datasance" }}./cmd/potctl/main.go{{ else }}./cmd/iofogctl/main.go{{ end }}' + goos: + - darwin + goarch: + - amd64 + - arm64 + flags: + - -tags=containers_image_openpgp,exclude_graphdriver_btrfs + ldflags: &release_ldflags + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.versionNumber={{ .Version }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.commit={{ .ShortCommit }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.date={{ .Date }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.platform={{ .Os }}/{{ .Arch }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.cliBinaryName={{ .Env.CLI_BINARY_NAME }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.cliCrdGroup={{ .Env.CLI_CRD_GROUP }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.cliApiVersion={{ .Env.CLI_API_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.cliCpCrName={{ .Env.CLI_CP_CR_NAME }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.imageRegistry={{ .Env.IMAGE_REGISTRY }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.cliDocsUrl={{ .Env.CLI_DOCS_URL }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.packageRepoBase={{ .Env.PACKAGE_REPO_BASE }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.ociSourceRepo={{ .Env.OCI_SOURCE_REPO }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.operatorTag={{ .Env.OPERATOR_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.routerTag={{ .Env.ROUTER_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerTag={{ .Env.CONTROLLER_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.natsTag={{ .Env.NATS_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.edgeletTag={{ .Env.EDGELET_IMAGE_TAG }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerVersion={{ .Env.CONTROLLER_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.edgeletVersion={{ .Env.EDGELET_IMAGE_TAG }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.edgeletReleaseBase={{ .Env.EDGELET_RELEASE_BASE }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.edgeletGitHubRepo={{ .Env.EDGELET_GITHUB_REPO }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.edgeletBinaryVersion={{ .Env.EDGELET_BINARY_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.debuggerTag=latest" + + - id: build_linux_amd64 + binary: '{{ if eq (.Env.FLAVOR) "datasance" }}potctl{{ else }}iofogctl{{ end }}' + main: '{{ if eq (.Env.FLAVOR) "datasance" }}./cmd/potctl/{{ else }}./cmd/iofogctl/{{ end }}' + goos: + - linux + goarch: + - amd64 + flags: + - -v + - -tags=containers_image_openpgp,exclude_graphdriver_btrfs + ldflags: + - -extldflags -static + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.versionNumber={{ .Version }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.commit={{ .ShortCommit }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.date={{ .Date }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.platform={{ .Os }}/{{ .Arch }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.cliBinaryName={{ .Env.CLI_BINARY_NAME }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.cliCrdGroup={{ .Env.CLI_CRD_GROUP }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.cliApiVersion={{ .Env.CLI_API_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.cliCpCrName={{ .Env.CLI_CP_CR_NAME }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.imageRegistry={{ .Env.IMAGE_REGISTRY }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.cliDocsUrl={{ .Env.CLI_DOCS_URL }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.packageRepoBase={{ .Env.PACKAGE_REPO_BASE }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.ociSourceRepo={{ .Env.OCI_SOURCE_REPO }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.operatorTag={{ .Env.OPERATOR_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.routerTag={{ .Env.ROUTER_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerTag={{ .Env.CONTROLLER_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.natsTag={{ .Env.NATS_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.edgeletTag={{ .Env.EDGELET_IMAGE_TAG }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.controllerVersion={{ .Env.CONTROLLER_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.edgeletVersion={{ .Env.EDGELET_IMAGE_TAG }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.edgeletReleaseBase={{ .Env.EDGELET_RELEASE_BASE }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.edgeletGitHubRepo={{ .Env.EDGELET_GITHUB_REPO }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.edgeletBinaryVersion={{ .Env.EDGELET_BINARY_VERSION }}" + - -s -w -X "github.com/eclipse-iofog/iofogctl/pkg/util.debuggerTag=latest" + + - id: build_linux_arm64 + binary: '{{ if eq (.Env.FLAVOR) "datasance" }}potctl{{ else }}iofogctl{{ end }}' + main: '{{ if eq (.Env.FLAVOR) "datasance" }}./cmd/potctl/{{ else }}./cmd/iofogctl/{{ end }}' + goos: + - linux + goarch: + - arm64 + env: + - CC=aarch64-linux-gnu-gcc + - CXX=aarch64-linux-gnu-g++ + - CGO_ENABLED=1 + flags: + - -v + - -tags=containers_image_openpgp,exclude_graphdriver_btrfs + ldflags: *release_ldflags + + - id: build_linux_arm + binary: '{{ if eq (.Env.FLAVOR) "datasance" }}potctl{{ else }}iofogctl{{ end }}' + main: '{{ if eq (.Env.FLAVOR) "datasance" }}./cmd/potctl/{{ else }}./cmd/iofogctl/{{ end }}' + goos: + - linux + goarch: + - arm + goarm: + - 6 + - 7 + env: + - CC=arm-linux-gnueabihf-gcc + - CXX=arm-linux-gnueabihf-g++ + - CGO_ENABLED=1 + flags: + - -v + - -tags=containers_image_openpgp,exclude_graphdriver_btrfs + ldflags: *release_ldflags + + - id: build_windows + binary: '{{ if eq (.Env.FLAVOR) "datasance" }}potctl{{ else }}iofogctl{{ end }}' + env: + - CGO_ENABLED=0 + main: '{{ if eq (.Env.FLAVOR) "datasance" }}./cmd/potctl/main.go{{ else }}./cmd/iofogctl/main.go{{ end }}' + goos: + - windows + goarch: + - amd64 + flags: + - -tags=containers_image_openpgp,exclude_graphdriver_btrfs + ldflags: *release_ldflags + +archives: + - id: linux + ids: + - build_linux_amd64 + - build_linux_arm64 + - build_linux_arm + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + formats: ['tar.gz'] + files: + - README.md + - LICENSE + - id: macos + ids: + - build_macos + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ if eq .Os `darwin` }}macos{{ else }}{{ .Os }}{{ end }}_{{ .Arch }}" + formats: ['tar.gz'] + files: + - README.md + - LICENSE + +checksum: + name_template: "{{.ProjectName}}-checksums.txt" + +snapshot: + version_template: "{{ .Version }}-SNAPSHOT" + +changelog: + disable: true + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + - '^dev:' + - 'README' + - Merge pull request + - Merge branch + +release: + github: + owner: '{{ if eq (.Env.FLAVOR) "datasance" }}Datasance{{ else }}eclipse-iofog{{ end }}' + name: '{{ if eq (.Env.FLAVOR) "datasance" }}potctl{{ else }}iofogctl{{ end }}' + draft: false + prerelease: auto + +brews: + - name: '{{ if eq (.Env.FLAVOR) "datasance" }}potctl{{ else }}iofogctl{{ end }}' + homepage: '{{ if eq (.Env.FLAVOR) "datasance" }}https://github.com/Datasance/potctl{{ else }}https://github.com/eclipse-iofog/iofogctl{{ end }}' + description: '{{ if eq (.Env.FLAVOR) "datasance" }}CLI for Datasance potctl{{ else }}CLI for ioFog{{ end }}' + goarm: 6 + repository: + owner: '{{ if eq (.Env.FLAVOR) "datasance" }}Datasance{{ else }}eclipse-iofog{{ end }}' + name: '{{ if eq (.Env.FLAVOR) "datasance" }}homebrew-potctl{{ else }}homebrew-iofogctl{{ end }}' + url_template: "https://github.com/{{ if eq (.Env.FLAVOR) \"datasance\" }}Datasance/potctl{{ else }}eclipse-iofog/iofogctl{{ end }}/releases/download/{{ .Tag }}/{{ .ArtifactName }}" + commit_author: + name: emirhandurmus + email: emirhan.durmus@datasance.com + directory: Formula + test: | + system "#{bin}/{{ .ProjectName }} version" + install: | + bin.install "{{ .ProjectName }}" + skip_upload: false + +nfpms: + - ids: ['build_linux_amd64', 'build_linux_arm64', 'build_linux_arm'] + homepage: '{{ if eq (.Env.FLAVOR) "datasance" }}https://github.com/Datasance/potctl{{ else }}https://github.com/eclipse-iofog/iofogctl{{ end }}' + description: '{{ if eq (.Env.FLAVOR) "datasance" }}CLI for Datasance potctl{{ else }}CLI for ioFog{{ end }}' + maintainer: Contributors to the Eclipse ioFog Project + vendor: Datasance + formats: + - deb + - rpm + - apk + overrides: + deb: + file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + rpm: + file_name_template: "{{ .ProjectName }}-{{ .Version }}-1.{{ if eq .Arch `amd64` }}x86_64{{ else if eq .Arch `arm64` }}aarch64{{ else }}{{ .Arch }}{{ end }}{{ if .Arm }}v{{ .Arm }}hl{{ end }}" + apk: + file_name_template: "{{ .ConventionalFileName }}" diff --git a/.packagecloud-publish.sh b/.packagecloud-publish.sh deleted file mode 100755 index 70f4cbcd5..000000000 --- a/.packagecloud-publish.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bash - -# This script publishes ".deb" and ".rpm" files in the ./dist -# dir to packagecloud. This script relies on the existence -# of the "packagecloud" binary: https://github.com/edgeworx/packagecloud -# The binary can be installed via: go install github.com/edgeworx/packagecloud@v0.1.0 - -set -e - -echo "" -echo "*************** Publish to packagecloud.io ***************" - -if [[ -z "$PACKAGECLOUD_TOKEN" ]]; then - echo "Must provide PACKAGECLOUD_TOKEN envar" 1>&2 - exit 1 -fi - -repo="${PACKAGECLOUD_REPO:-iofog/iofogctl-snapshots}" -echo "Using packagecloud repo: $repo" - -pushd ./dist > /dev/null -echo "Using dist dir: $PWD" - - -failed_push_file='./failed_packagecloud_push' -echo -n "" > $failed_push_file - -# add some stutter to avoid overloading packagecloud API -function sleepStutter() { - sleep "0.$((50 + RANDOM % 300))s" -} - -function deb() { - packages=$(ls | grep .deb) - - echo "" - echo "*************** Publish .deb ***************" - echo "deb packages to publish..." - echo "$packages" - echo "" - declare -a distro_versions=( - "ubuntu/focal" "ubuntu/xenial" "ubuntu/bionic" "ubuntu/trusty" "ubuntu/jammy" - "debian/stretch" "debian/buster" "debian/bullseye" - "raspbian/stretch" "raspbian/buster" "raspbian/bullseye" - "any/any" - ) - - for package in $packages; do - for distro_version in "${distro_versions[@]}"; do - sleepStutter - repo_full_path="$repo/$distro_version" - echo $repo_full_path - { - packagecloud push --overwrite "${repo_full_path}" "${package}" 2> >(tee -a $failed_push_file >&2) - } & - done - done - - wait -} - -function rpm() { - packages=$(ls | grep .rpm) - - echo "" - echo "*************** Publish .rpm ***************" - echo "rpm packages to publish..." - echo "$packages" - echo "" - - - declare -a distro_versions=( - "fedora/23" "fedora/24" "fedora/30" "fedora/31" - "el/6" "el/7" "el/8" - "rpm_any/rpm_any" - ) - - for package in $packages; do - for distro_version in "${distro_versions[@]}"; do - sleepStutter - repo_full_path="$repo/$distro_version" - { - packagecloud push --overwrite "${repo_full_path}" "${package}" 2> >(tee -a $failed_push_file >&2) - } & - done - done - - wait -} - -deb & -sleep 0.1s # give the deb func time to do its output -rpm & - -wait - -echo "" -if [ -s $failed_push_file ]; then - # There's content in #failed_push_file... so we need to output it and exit 1. - echo "*************** Failures (from $failed_push_file) ***************" - echo "" - cat $failed_push_file - echo "" - exit 1 -else - echo "*************** SUCCESS ***************" -fi \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2356dbd..ee20f22ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,69 @@ # Changelog -## [Unreleased] +All notable changes to potctl / iofogctl are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [3.8.0-rc.1] — June 2026 + + +First greenfield v3.8 release candidate. Dual-flavor build (`potctl` / `iofogctl`) from a single codebase; no in-place upgrade from potctl- or v3.7. + +### Added + +- Dual mirror: canonical [Datasance/potctl](https://github.com/Datasance/potctl), upstream [eclipse-iofog/iofogctl](https://github.com/eclipse-iofog/iofogctl) +- `KubernetesControlPlane`, `RemoteControlPlane`, and `LocalControlPlane` resource kinds for v3.8 deployments +- **edgelet** platform for edge node agents (replaces Java `iofog-agent`) +- Embedded auth mode (`auth.mode: embedded|external`) — no Keycloak YAML blocks +- Unified `.goreleaser.yml`; release flavor derived from GitHub repo (`Datasance/potctl` → potctl, `eclipse-iofog/iofogctl` → iofogctl) +- Package repositories: [downloads.datasance.com](https://downloads.datasance.com/) (potctl), [iofog.datasance.com](https://iofog.datasance.com/) (iofogctl) +- Config directory `~/.iofog/v3` for both flavors +- Root `NOTICE` file (no per-file copyright headers) +- GitHub Actions CI, govulncheck, and CodeQL workflows +- SDK log streaming: `DialMicroserviceLogs`, `DialSystemMicroserviceLogs`, and `DialFogLogs` with `LogSession` (requires iofog-go-sdk with log session support) +- Exec dial status callback: `DialExecOptions.OnStatusLine` surfaces agent connection progress during exec handshake + +### Changed + +- Build-time ldflags: `edgeletTag` / `edgeletVersion` replace `agentTag` / `agentVersion` +- Operator deploy via Kubernetes API (direct apply) — no Helm repo ldflag +- Ingress service name: `controller` (was `iofog-controller`) +- Embedded assets via `go:embed` (replaces `go.rice`) +- CI migrated from Azure Pipelines to GitHub Actions +- Remote `logs` commands use SDK WebSocket sessions instead of `internal/util/websocket` +- Exec sessions close once via idempotent `ExecSession.Close()`; shell `exit` no longer prints benign close errors +- Log `--tail` maximum aligned with Controller limit (5000) +- `exec agent` auto-provisions fog debug exec when missing, polls until the debug container is `RUNNING`, then connects + +### Removed + +- `FogType` field (use `systemAgent.arch`) +- Keycloak auth blocks in deployment YAML +- `agentTag` ldflag and Java agent install paths +- `HELM_REPO_BASE_URL` ldflag +- packagecloud.io install scripts and documentation +- README logo image (`iofogctl-logo.png`) +- `attach exec microservice` / `detach exec microservice` commands (Plan 17 direct exec dial) +- Unused `internal/util/websocket` and `internal/util/terminal` packages + +### Security + +- govulncheck and CodeQL run with the same `-tags` as release builds (`containers_image_openpgp,exclude_graphdriver_btrfs`) + +### Component pairing + +| Component | Pin | +|-----------|-----| +| CLI | `v3.8.0-rc.1` | +| operator | `3.8.0-rc.1` | +| controller | `3.8.0-rc.4` | +| router | `3.8.0-rc.1` | +| nats | `2.14.2-rc.2` | +| edgelet binary | `v1.0.0-rc.6` | +| edgelet image | `ghcr.io//edgelet:1.0.0-rc.6` | + +## Pre-3.8 history ## [v3.0.1] - 27 May 2022 * Updated openjdk-11 installation on Ubuntu @@ -199,7 +262,8 @@ * Add client package to the repo * Re-organize the repo to maintain multiple packages -[Unreleased]: https://github.com/eclipse-iofog/iofogctl/compare/v3.0.0-beta8..HEAD +[Unreleased]: https://github.com/Datasance/potctl/compare/v3.8.0-rc.1...HEAD +[3.8.0-rc.1]: https://github.com/Datasance/potctl/releases/tag/v3.8.0-rc.1 [v3.0.0-beta8]: https://github.com/eclipse-iofog/iofogctl/compare/v3.0.0-beta7..v3.0.0-beta8 [v3.0.0-beta7]: https://github.com/eclipse-iofog/iofogctl/compare/v3.0.0-beta6..v3.0.0-beta7 [v3.0.0-beta6]: https://github.com/eclipse-iofog/iofogctl/compare/v3.0.0-beta5..v3.0.0-beta6 diff --git a/CONTRIBUTING b/CONTRIBUTING index 89d72e8c3..0037d7fc6 100644 --- a/CONTRIBUTING +++ b/CONTRIBUTING @@ -32,7 +32,54 @@ contributions are always welcome! ### Git hooks -Please run `make init` in order to initialise git hooks +Run `make bootstrap` to install dependencies and configure git hooks. + +The pre-commit hook rebuilds both CLI binaries and regenerates cobra markdown under `docs/iofogctl_md/` and `docs/potctl_md/`. + +### Build from source + +Build one flavor: + +```bash +make iofogctl +make potctl +``` + +Or set `FLAVOR` explicitly: + +```bash +make FLAVOR=iofog build +make FLAVOR=datasance build +``` + +## Continuous integration + +CI runs on **GitHub Actions** (`.github/workflows/`). Azure Pipelines and the legacy `pipeline/` tree were retired in v3.8 Phase 2. + +- **Datasance/potctl:** PR builds both `potctl` and `iofogctl` +- **eclipse-iofog/iofogctl:** PR builds `iofogctl` only — see `.cursor/cli/docs/eclipse-ci-sync.md` + +Local checks: `make lint`, `make test-unit`, `make grep-gates`, `make security-code`, `make vulncheck` + +## Release builds + +GoReleaser uses a single root `.goreleaser.yml`. Each mirror releases one binary: + +| GitHub repo | Binary | +|-------------|--------| +| `Datasance/potctl` | `potctl` | +| `eclipse-iofog/iofogctl` | `iofogctl` | + +`script/goreleaser-env.sh` picks flavor from `GITHUB_REPOSITORY` (default `iofog`). Component pins always come from `versions.mk`. CI sets `GITHUB_REPOSITORY` automatically; for local release smoke: + +```bash +GITHUB_REPOSITORY=Datasance/potctl script/goreleaser-release.sh release --snapshot --clean +GITHUB_REPOSITORY=eclipse-iofog/iofogctl script/goreleaser-release.sh release --snapshot --clean +``` + +Override flavor explicitly when needed: `FLAVOR=datasance script/goreleaser-release.sh check` + +Release workflow requires `HOMEBREW_TAP_GITHUB_TOKEN` (PAT with write access to the mirror's Homebrew tap repo). ## Eclipse Contributor Agreement diff --git a/Makefile b/Makefile index d0a1f670d..17d15ee0d 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,16 @@ SHELL = /bin/bash OS = $(shell uname -s | tr '[:upper:]' '[:lower:]') +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) # Build variables -BINARY_NAME = iofogctl +include versions.mk + +# Build variables +FLAVOR ?= iofog +BINARY_NAME ?= iofogctl BUILD_DIR ?= bin -PACKAGE_DIR = cmd/iofogctl +PACKAGE_DIR ?= cmd/iofogctl GOTAGS ?= containers_image_openpgp,exclude_graphdriver_btrfs export CGO_ENABLED=1 LATEST_TAG = $(shell git for-each-ref refs/tags --sort=-taggerdate --format='%(refname)' | tail -n1 | sed "s|refs/tags/||") @@ -18,19 +24,55 @@ VERSION ?= $(MAJOR).$(MINOR).$(PATCH)$(SUFFIX) COMMIT ?= $(shell git rev-parse HEAD 2>/dev/null) BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) PREFIX = github.com/eclipse-iofog/iofogctl/pkg/util + +ifeq ($(FLAVOR),datasance) + CLI_BINARY_NAME = potctl + CLI_CRD_GROUP = datasance.com + CLI_API_VERSION = datasance.com/v3 + CLI_CP_CR_NAME = pot + IMAGE_REGISTRY = ghcr.io/datasance + CLI_DOCS_URL = https://docs.datasance.com + PACKAGE_REPO_BASE = https://downloads.datasance.com + OCI_SOURCE_REPO = https://github.com/Datasance/potctl + EDGELET_RELEASE_BASE = https://github.com/Datasance/edgelet/releases/download + EDGELET_GITHUB_REPO = Datasance/edgelet +else + CLI_BINARY_NAME = iofogctl + CLI_CRD_GROUP = iofog.org + CLI_API_VERSION = iofog.org/v3 + CLI_CP_CR_NAME = iofog + IMAGE_REGISTRY = ghcr.io/eclipse-iofog + CLI_DOCS_URL = https://iofog.org + PACKAGE_REPO_BASE = https://iofog.datasance.com + OCI_SOURCE_REPO = https://github.com/eclipse-iofog/iofogctl + EDGELET_RELEASE_BASE = https://github.com/eclipse-iofog/edgelet/releases/download + EDGELET_GITHUB_REPO = eclipse-iofog/edgelet +endif + LDFLAGS += -X $(PREFIX).versionNumber=$(VERSION) -X $(PREFIX).commit=$(COMMIT) -X $(PREFIX).date=$(BUILD_DATE) -X $(PREFIX).platform=$(GOOS)/$(GOARCH) -LDFLAGS += -X $(PREFIX).operatorTag=3.7.1 -LDFLAGS += -X $(PREFIX).routerTag=3.7.0 -LDFLAGS += -X $(PREFIX).controllerTag=3.7.1 -LDFLAGS += -X $(PREFIX).agentTag=3.7.0 -LDFLAGS += -X $(PREFIX).controllerVersion=3.7.1 -LDFLAGS += -X $(PREFIX).agentVersion=3.7.0 +LDFLAGS += -X $(PREFIX).cliBinaryName=$(CLI_BINARY_NAME) +LDFLAGS += -X $(PREFIX).cliCrdGroup=$(CLI_CRD_GROUP) +LDFLAGS += -X $(PREFIX).cliApiVersion=$(CLI_API_VERSION) +LDFLAGS += -X $(PREFIX).cliCpCrName=$(CLI_CP_CR_NAME) +LDFLAGS += -X $(PREFIX).imageRegistry=$(IMAGE_REGISTRY) +LDFLAGS += -X $(PREFIX).cliDocsUrl=$(CLI_DOCS_URL) +LDFLAGS += -X $(PREFIX).packageRepoBase=$(PACKAGE_REPO_BASE) +LDFLAGS += -X $(PREFIX).ociSourceRepo=$(OCI_SOURCE_REPO) +LDFLAGS += -X $(PREFIX).operatorTag=$(OPERATOR_VERSION) +LDFLAGS += -X $(PREFIX).routerTag=$(ROUTER_VERSION) +LDFLAGS += -X $(PREFIX).controllerTag=$(CONTROLLER_VERSION) +LDFLAGS += -X $(PREFIX).natsTag=$(NATS_VERSION) +LDFLAGS += -X $(PREFIX).edgeletTag=$(EDGELET_IMAGE_TAG) +LDFLAGS += -X $(PREFIX).controllerVersion=$(CONTROLLER_VERSION) +LDFLAGS += -X $(PREFIX).edgeletVersion=$(EDGELET_IMAGE_TAG) +LDFLAGS += -X $(PREFIX).edgeletReleaseBase=$(EDGELET_RELEASE_BASE) +LDFLAGS += -X $(PREFIX).edgeletGitHubRepo=$(EDGELET_GITHUB_REPO) +LDFLAGS += -X $(PREFIX).edgeletBinaryVersion=$(EDGELET_BINARY_VERSION) LDFLAGS += -X $(PREFIX).debuggerTag=latest -LDFLAGS += -X $(PREFIX).natsTag=2.12.4 -LDFLAGS += -X $(PREFIX).repo=ghcr.io/eclipse-iofog -GO_SDK_MODULE = iofog-go-sdk/v3@v3.7.0 -OPERATOR_MODULE = iofog-operator/v3@v3.7.1 -REPORTS_DIR ?= reports + +GOLANGCI_LINT_VERSION ?= v2.12.2 +GOVULNCHECK_VERSION ?= v1.1.4 +GOSEC_SCOPE ?= ./cmd/... ./internal/... ./pkg/... TEST_RESULTS ?= TEST-iofogctl.txt TEST_REPORT ?= TEST-iofogctl.xml @@ -49,15 +91,22 @@ verify-gpgme: exit 1; \ fi +.PHONY: potctl +potctl: ## Build potctl binary + @$(MAKE) FLAVOR=datasance BINARY_NAME=potctl PACKAGE_DIR=cmd/potctl build + +.PHONY: iofogctl +iofogctl: ## Build iofogctl binary + @$(MAKE) FLAVOR=iofog BINARY_NAME=iofogctl PACKAGE_DIR=cmd/iofogctl build + .PHONY: build build: GOARGS += -tags "$(GOTAGS)" -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME) build: fmt ## Build the binary - @cd pkg/util && rice embed-go @go build -v $(GOARGS) $(PACKAGE_DIR)/main.go .PHONY: install install: ## Install the binary - @GOBIN=$$(go env GOPATH)/bin go install -tags "$(GOTAGS)" -ldflags "$(LDFLAGS)" ./cmd/iofogctl/ + @GOBIN=$$(go env GOPATH)/bin go install -tags "$(GOTAGS)" -ldflags "$(LDFLAGS)" ./$(PACKAGE_DIR)/ .PHONY: lint lint: golangci-lint fmt ## Lint the source @@ -67,7 +116,7 @@ golangci-lint: ## Install golangci ifeq (, $(shell which golangci-lint)) @{ \ set -e ;\ - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.4 ;\ + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) ;\ } GOLANGCI_LINT=$(GOBIN)/golangci-lint else @@ -78,6 +127,37 @@ endif fmt: ## Format the source @gofmt -s -w . +.PHONY: test-unit +test-unit: ## Run unit tests (short mode) + go test ./internal/... ./pkg/... -short -count=1 -tags "$(GOTAGS)" -ldflags "$(LDFLAGS)" + +.PHONY: smoke +smoke: build ## Smoke test: version and help + @$(BUILD_DIR)/$(BINARY_NAME) version + @$(BUILD_DIR)/$(BINARY_NAME) --help + @$(BUILD_DIR)/$(BINARY_NAME) deploy --help + +.PHONY: grep-gates +grep-gates: ## Fail on hardcoded flavor strings in internal/ + @chmod +x scripts/ci/grep-gates.sh + @scripts/ci/grep-gates.sh + +.PHONY: vulncheck +vulncheck: ## Run govulncheck on module paths + @if ! command -v govulncheck >/dev/null 2>&1; then \ + go install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION); \ + fi + @chmod +x scripts/vulncheck.sh + @GOTAGS="$(GOTAGS)" scripts/vulncheck.sh + @go mod verify + +.PHONY: security-code +security-code: ## Run gosec static analysis + @if ! command -v gosec >/dev/null 2>&1; then \ + go install github.com/securego/gosec/v2/cmd/gosec@latest; \ + fi + @gosec -exclude-dir=build $(GOSEC_SCOPE) + .PHONY: test test: ## Run unit tests mkdir -p $(REPORTS_DIR) diff --git a/NOTICE b/NOTICE index b336fbf9c..d7145cb8d 100644 --- a/NOTICE +++ b/NOTICE @@ -1,12 +1,15 @@ -# Notices for Eclipse ioFog +# Notices -This content is produced and maintained by the Eclipse ioFog project. +This content is produced and maintained by the Eclipse ioFog project and +Datasance contributors to the shared `iofogctl` / `potctl` CLI mirror. -* Project home: https://projects.eclipse.org/projects/iot.iofog +* Eclipse ioFog project home: https://projects.eclipse.org/projects/iot.iofog +* Datasance PoT: https://datasance.com ## Trademarks Eclipse ioFog is a trademark of the Eclipse Foundation. +Datasance and PoT are trademarks of Datasance. ## Copyright @@ -26,6 +29,8 @@ SPDX-License-Identifier: EPL-2.0 The project maintains the following source code repositories: +* https://github.com/eclipse-iofog/iofogctl +* https://github.com/Datasance/potctl * https://github.com/eclipse-iofog * http://git.eclipse.org/c/iofog/org.eclipse.iofog.git diff --git a/README.md b/README.md index 3698c109d..9a7f1298c 100644 --- a/README.md +++ b/README.md @@ -1,170 +1,123 @@ -![iofogctl-logo](iofogctl-logo.png?raw=true "iofogctl logo") +[![CLI CI](https://github.com/eclipse-iofog/iofogctl/actions/workflows/ci.yml/badge.svg)](https://github.com/eclipse-iofog/iofogctl/actions/workflows/ci.yml) +[![Release](https://img.shields.io/github/v/release/eclipse-iofog/iofogctl?include_prereleases)](https://github.com/eclipse-iofog/iofogctl/releases) +[![Go](https://img.shields.io/badge/Go-1.26.4-blue.svg)](https://go.dev/) +[![License](https://img.shields.io/badge/License-EPL--2.0-blue.svg)](LICENSE) +[![govulncheck](https://github.com/eclipse-iofog/iofogctl/actions/workflows/govulncheck.yml/badge.svg)](https://github.com/eclipse-iofog/iofogctl/actions/workflows/govulncheck.yml) +[![CodeQL](https://github.com/eclipse-iofog/iofogctl/actions/workflows/codeql.yml/badge.svg)](https://github.com/eclipse-iofog/iofogctl/actions/workflows/codeql.yml) -`iofogctl` is a CLI for the installation, configuration, and operation of ioFog -[Edge Compute Networks](https://iofog.org/docs/2/getting-started/core-concepts.html) (ECNs). -It can be used to remotely manage multiple ECNs from a single host. It is built for ioFog users and DevOps engineers -wanting to manage ECNs. It is modelled on existing tools such as Terraform or kubectl that can be used to automate -infrastructure-as-code. +[![Linux amd64](https://img.shields.io/badge/linux--amd64-supported-2ea44f?style=flat&logo=linux&logoColor=white)](https://github.com/eclipse-iofog/iofogctl/releases) +[![Linux arm64](https://img.shields.io/badge/linux--arm64-supported-2ea44f?style=flat&logo=linux&logoColor=white)](https://github.com/eclipse-iofog/iofogctl/releases) +[![Linux armv6](https://img.shields.io/badge/linux--armv6-supported-2ea44f?style=flat&logo=linux&logoColor=white)](https://github.com/eclipse-iofog/iofogctl/releases) +[![Linux armv7](https://img.shields.io/badge/linux--armv7-supported-2ea44f?style=flat&logo=linux&logoColor=white)](https://github.com/eclipse-iofog/iofogctl/releases) -## Install +[![macOS amd64](https://img.shields.io/badge/macos--amd64-supported-2ea44f?style=flat&logo=apple&logoColor=white)](https://github.com/eclipse-iofog/iofogctl/releases) +[![macOS arm64](https://img.shields.io/badge/macos--arm64-supported-2ea44f?style=flat&logo=apple&logoColor=white)](https://github.com/eclipse-iofog/iofogctl/releases) +[![Windows amd64](https://img.shields.io/badge/windows--amd64-supported-2ea44f?style=flat&logo=windows&logoColor=white)](https://github.com/eclipse-iofog/iofogctl/releases) -#### Mac +Upstream: [eclipse-iofog/iofogctl](https://github.com/eclipse-iofog/iofogctl) · Development mirror: [Datasance/potctl](https://github.com/Datasance/potctl) -Mac users can use Homebrew: +**iofogctl** and **potctl** are dual-flavor CLIs for installing, configuring, and operating ioFog [Edge Compute Networks](https://iofog.org/docs/2/getting-started/core-concepts.html) (ECNs). v3.8 is a greenfield release: configuration lives under `~/.iofog/v3`, and there is no in-place upgrade path from legacy potctl- or v3.7 deployments. -```bash -brew tap eclipse-iofog/iofogctl -brew install iofogctl -``` +Release binaries ship for **linux** (amd64, arm64, armv6, armv7), **macOS** (amd64, arm64), and **Windows** (amd64). Built with **Go 1.26.4** (see `go.mod`). -#### Linux +## Install — iofogctl -The Debian package can be installed like so: -```bash -https://packagecloud.io/install/repositories/iofog/iofogctl/script.deb.sh | sudo bash -sudo apt install iofogctl -``` +Package repository: [iofog.datasance.com](https://iofog.datasance.com/) -And similarly, the RPM package can be installed like so: -``` -https://packagecloud.io/install/repositories/iofog/iofogctl/script.rpm.sh | sudo bash -sudo apt install iofogctl +**Linux (DEB or RPM):** + +```bash +wget -q -O - https://iofog.datasance.com/iofogctl_installer.sh | sudo bash +sudo apt install -y iofogctl # Debian/Ubuntu +# or +sudo yum install -y iofogctl # RHEL/CentOS/Fedora ``` -## Usage +**macOS (Homebrew):** -### Documentation +```bash +brew tap eclipse-iofog/homebrew-iofogctl +brew install iofogctl +``` -The best way to learn how to use `iofogctl` is through the [iofog.org](https://iofog.org/docs/2/getting-started/quick-start-local.html) learning resources. +## Install — potctl -#### Quick Start +Package repository: [downloads.datasance.com](https://downloads.datasance.com/) -See all iofogctl options +**Linux (DEB or RPM):** -``` -iofogctl --help +```bash +wget -q -O - https://downloads.datasance.com/potctl_installer.sh | sudo bash +sudo apt install -y potctl # Debian/Ubuntu +# or +sudo yum install -y potctl # RHEL/CentOS/Fedora ``` -Current options include: - -``` - _ ____ __ __ - (_)___ / __/___ ____ _____/ /_/ / - / / __ \/ /_/ __ \/ __ `/ ___/ __/ / - / / /_/ / __/ /_/ / /_/ / /__/ /_/ / - /_/\____/_/ \____/\__, /\___/\__/_/ - /____/ - - - -Welcome to the cool new iofogctl Cli! - -Use `iofogctl version` to display the current version. - -Usage: - iofogctl [flags] - iofogctl [command] - -Available Commands: - attach Attach one ioFog resource to another - completion Generate the autocompletion script for the specified shell - configure Configure iofogctl or ioFog resources - connect Connect to an existing Control Plane - create Create a resource - delete Delete an existing ioFog resource - deploy Deploy Edge Compute Network components on existing infrastructure - describe Get detailed information of an existing resources - detach Detach one ioFog resource from another - disconnect Disconnect from an ioFog cluster - exec Connect to an Exec Session of a resource - get Get information of existing resources - help Help about any command - legacy Execute commands using legacy CLI - logs Get log contents of deployed resource - move Move an existing resources inside the current Namespace - prune prune ioFog resources - rebuild Rebuilds a microservice or system-microservice - rename Rename the iofog resources that are currently deployed - rollback Rollback ioFog resources - start Starts a resource - stop Stops a resource - upgrade Upgrade ioFog resources - version Get CLI application version - view Open ECN Viewer - -Flags: - --detached Use/Show detached resources - --debug Toggle for displaying verbose output of API clients (HTTP and SSH) - -h, --help help for iofogctl - -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl - -Use "iofogctl [command] --help" for more information about a command. +**macOS (Homebrew):** +```bash +brew tap Datasance/homebrew-potctl +brew install potctl ``` -### Autocomplete +## Edge node agent — edgelet -If you are running BASH or ZSH, iofogctl comes with shell autocompletion scripts. -In order to generate those scripts, run: +v3.8 edge nodes run **edgelet**, not the legacy Java `iofog-agent`. Deploy edge nodes with the CLI (`deploy -f` manifest) or install the edgelet binary directly: ```bash -iofogctl autocomplete bash +curl -fsSL https://github.com/eclipse-iofog/edgelet/releases/download/v1.0.0-rc.6/install.sh -o install.sh +chmod +x install.sh +sudo ./install.sh --version=v1.0.0-rc.6 ``` -OR + +Eclipse canonical: [eclipse-iofog/edgelet](https://github.com/eclipse-iofog/edgelet/releases) · Datasance mirror: [Datasance/edgelet](https://github.com/Datasance/edgelet/releases) + +## Usage ```bash -iofogctl autocomplete zsh +iofogctl version +iofogctl connect --help +iofogctl deploy -f ecn.yaml +# potctl is the Datasance-flavor equivalent (same commands, different binary name) ``` -Then follow the instructions output by the command. +Documentation: -Example: -```bash -iofogctl autocomplete bash -✔ $HOME/.iofog/completion.bash.sh generated -Run `source $HOME/.iofog/completion.bash.sh` to update your current session -Add `source $HOME/.iofog/completion.bash.sh` to your bash profile to have it saved +- **iofogctl:** [iofog.org](https://iofog.org/docs) +- **potctl:** [docs.datasance.com](https://docs.datasance.com) -source $HOME/.iofog/completion.bash.sh -echo "$HOME/.iofog/completion.bash.sh" >> $HOME/.bash_profile -``` +Shell autocompletion: `iofogctl autocomplete bash` (or `zsh`), then follow the printed instructions. -## Build from Source +## Build from source -This project uses go modules so it must be built from outside of your $GOPATH. +Requires Go **1.26.4+** (matches the Go badge and `go.mod`). Build outside `$GOPATH` (Go modules): -Go 1.19+ is a prerequisite. Install all other dependancies with: -``` -make bootstrap +```bash +make FLAVOR=iofog build # iofogctl → bin/iofogctl +make FLAVOR=datasance build # potctl → bin/potctl +# or +make iofogctl +make potctl ``` -Make sure that your `$PATH` contains `$GOBIN`, otherwise `make build` will fail on the basis that command `rice` is not found. -See all `make` commands by running: -``` -make -``` +Install to `$GOPATH/bin`: -To build and install, go ahead and run: -``` -make build install -iofogctl --help +```bash +make FLAVOR=iofog install ``` -iofogctl is installed in `/usr/local/bin` - -## Running Tests +## Running tests -Run project unit tests: -``` +```bash make test ``` -This will output a JUnit compatible file into `reports/TEST-iofogctl.xml` that can be imported in most CI systems. - -## Embed assets +Unit tests (short mode): -Run project build: -``` -make build +```bash +make test-unit ``` + +## License + +[EPL-2.0](LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..b714d614c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,58 @@ +# Security Policy + +## Supported versions + +| Version | Supported | +|---------|-----------| +| `v3.8.0-rc.1` and later pre-releases on `develop` | Yes | +| v3.7 / legacy Java agent / `iofog-agent` paths | No | + +This repository builds two CLI flavors from one tree: **iofogctl** (Eclipse upstream) and **potctl** (Datasance). Security support applies to both at the same tagged versions. + +## Reporting a vulnerability + +If you believe you have found a security issue in iofogctl or potctl: + +1. **Do not** open a public GitHub issue for exploitable vulnerabilities. +2. Email **security@datasance.com** with: + - A description of the issue and impact + - Steps to reproduce (proof-of-concept if available) + - Affected version / commit, CLI flavor (`iofogctl` or `potctl`), and platform +3. We aim to acknowledge reports within **5 business days** and provide a remediation timeline when confirmed. + +For non-security bugs, use the public issue tracker or `CONTRIBUTING`. + +## Security gates (maintainers) + +Before release tags, run: + +```bash +make security-code # gosec on ./cmd ./internal ./pkg +make vulncheck # govulncheck@v1.1.4 + go mod verify +``` + +- **gosec** is intentionally **not** in golangci-lint; static analysis is scoped to CLI module trees. +- **govulncheck** scans `./cmd/... ./internal/... ./pkg/...`. Goal: **zero** vulnerabilities affecting call paths. +- CI: [`.github/workflows/ci.yml`](.github/workflows/ci.yml) (security job), [`.github/workflows/govulncheck.yml`](.github/workflows/govulncheck.yml) (on `go.sum` push, daily cron, manual dispatch), [`.github/workflows/codeql.yml`](.github/workflows/codeql.yml). + +## Build tags + +Build, test, goreleaser, and govulncheck share the same Go build tags via `GOTAGS` in the Makefile (default: `containers_image_openpgp,exclude_graphdriver_btrfs`). This keeps vulnerability scanning aligned with how binaries are compiled without requiring optional cgo dependencies (`libgpgme-dev`, `libbtrfs-dev`) on CI. + +## Known vulnerability exceptions + +| GO ID | CVE | Component | Rationale | Fix timeline | +|-------|-----|-----------|-----------|--------------| +| *(none)* | | | | | + +No documented exceptions at launch. `make vulncheck` must pass with zero findings affecting CLI call paths. + +## Exception policy + +New exceptions require: + +1. Entry in the table above (GO ID, CVE if any, component, rationale, fix timeline). +2. Matching ID in `scripts/vulncheck.sh` `ALLOWED_VULNS`. +3. Brief note under **Known limitations** in `CHANGELOG.md` at next release. + +Undocumented findings **fail** `make vulncheck` and CI. diff --git a/assets/agent/check_prereqs.sh b/assets/agent/check_prereqs.sh deleted file mode 100755 index a6b148d4a..000000000 --- a/assets/agent/check_prereqs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -x - -# Check can sudo without password -if ! $(sudo ls /tmp/ > /dev/null); then - MSG="Unable to successfully use sudo with user $USER on this host.\nUser $USER must be in sudoers group and using sudo without password must be enabled.\nPlease see iofog.org documentation for more details." - echo $MSG - exit 1 -fi \ No newline at end of file diff --git a/assets/agent/init.sh b/assets/agent/init.sh deleted file mode 100755 index 630898c81..000000000 --- a/assets/agent/init.sh +++ /dev/null @@ -1,296 +0,0 @@ -#!/bin/sh -# Script to detect Linux distribution and version -# Used as a precursor for system-specific installations - -# Exit on error and print commands for debugging -set -e -set -x - -# Define user variable -user="$(id -un 2>/dev/null || true)" - -# Check if a command exists -command_exists() { - command -v "$@" > /dev/null 2>&1 -} - -# Detect the Linux distribution -get_distribution() { - lsb_dist="" - dist_version="" - - # Every system that we officially support has /etc/os-release - if [ -r /etc/os-release ]; then - - lsb_dist="$(. /etc/os-release && echo "$ID")" - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')" - else - echo "Error: Unsupported Linux distribution! /etc/os-release not found." - exit 1 - fi - - echo "# Detected distribution: $lsb_dist (version: $dist_version)" -} - -# Check if this is a forked Linux distro -check_forked() { - # Skip if lsb_release doesn't exist - if ! command_exists lsb_release; then - return - fi - - # Check if the `-u` option is supported - set +e - lsb_release -a > /dev/null 2>&1 - lsb_release_exit_code=$? - set -e - - # Check if the command has exited successfully, it means we're in a forked distro - if [ "$lsb_release_exit_code" = "0" ]; then - # Get the upstream release info - current_lsb_dist=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]') - current_dist_version=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]') - - # Print info about current distro - echo "You're using '$current_lsb_dist' version '$current_dist_version'." - - # Check if current is different from detected (indicating a fork) - if [ "$current_lsb_dist" != "$lsb_dist" ] || [ "$current_dist_version" != "$dist_version" ]; then - echo "Upstream release is '$lsb_dist' version '$dist_version'." - fi - else - # Additional checks for specific distros that might not be properly detected - if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then - if [ "$lsb_dist" = "osmc" ]; then - # OSMC runs Raspbian - lsb_dist=raspbian - else - # We're Debian and don't even know it! - lsb_dist=debian - fi - # Get Debian version and map it to codename - dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" - case "$dist_version" in - 14) - dist_version="forky" - ;; - 13) - dist_version="trixie" - ;; - 12) - dist_version="bookworm" - ;; - 11) - dist_version="bullseye" - ;; - 10) - dist_version="buster" - ;; - 9) - dist_version="stretch" - ;; - 8|'Kali Linux 2') - dist_version="jessie" - ;; - 7) - dist_version="wheezy" - ;; - esac - elif [ -r /etc/redhat-release ] && [ -z "$lsb_dist" ]; then - lsb_dist=redhat - # Extract version from redhat-release file - dist_version="$(sed 's/.*release \([0-9.]*\).*/\1/' /etc/redhat-release)" - fi - fi -} - -# Set up sudo command if necessary -setup_sudo() { - sh_c='sh -c' - if [ "$user" != 'root' ]; then - if command_exists sudo; then - sh_c='sudo -E sh -c' - elif command_exists su; then - sh_c='su -c' - else - echo "Error: this installer needs the ability to run commands as root." - echo "We are unable to find either 'sudo' or 'su' available to make this happen." - exit 1 - fi - fi - echo "# Using command executor: $sh_c" -} - -# Refine distribution version detection based on the distro -refine_distribution_version() { - case "$lsb_dist" in - ubuntu) - if command_exists lsb_release; then - dist_version="$(lsb_release --codename | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then - - dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")" - fi - ;; - - debian|raspbian) - # If we only have a number, map it to a codename for better recognition - if echo "$dist_version" | grep -qE '^[0-9]+$'; then - case "$dist_version" in - 14) - dist_version="forky" - ;; - 13) - dist_version="trixie" - ;; - 12) - dist_version="bookworm" - ;; - 11) - dist_version="bullseye" - ;; - 10) - # Handle special case for Buster - dist_version="buster" - if [ "$user" = 'root' ]; then - apt-get update --allow-releaseinfo-change || true - elif command_exists sudo; then - sudo apt-get update --allow-releaseinfo-change || true - fi - ;; - 9) - dist_version="stretch" - ;; - 8) - dist_version="jessie" - ;; - 7) - dist_version="wheezy" - ;; - esac - fi - ;; - - centos|rhel|fedora|ol) - # Make sure we have a version number - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - if [ -z "$dist_version" ] && [ -r /etc/redhat-release ]; then - dist_version="$(sed 's/.*release \([0-9.]*\).*/\1/' /etc/redhat-release)" - fi - ;; - - sles|opensuse) - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - # Fallback for older versions - if [ -z "$dist_version" ] && [ -r /etc/SuSE-release ]; then - dist_version="$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = //')" - fi - # Ensure version is in the correct format (e.g., 15.4 for SLES 15 SP4) - if [ -n "$dist_version" ]; then - # Remove any non-numeric characters except dots - dist_version="$(echo "$dist_version" | sed 's/[^0-9.]//g')" - fi - # Normalize distribution name - if [ "$lsb_dist" = "sles" ]; then - lsb_dist="sles" - elif [ "$lsb_dist" = "opensuse" ]; then - lsb_dist="opensuse" - fi - ;; - - *) - if command_exists lsb_release; then - dist_version="$(lsb_release --release | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - ;; - esac -} - -# Detect init system -detect_init_system() { - if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then - INIT_SYSTEM="systemd" - elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then - INIT_SYSTEM="upstart" - elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then - INIT_SYSTEM="openrc" - elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then - INIT_SYSTEM="s6" - elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then - INIT_SYSTEM="runit" - elif [ -d /etc/init.d ]; then - INIT_SYSTEM="sysvinit" - else - INIT_SYSTEM="unknown" - fi - export INIT_SYSTEM - echo "# Detected init system: $INIT_SYSTEM" -} - -# Detect package type (deb, rpm, or other) -detect_package_type() { - case "$lsb_dist" in - debian|ubuntu|raspbian|mendel) - PACKAGE_TYPE="deb" - ;; - fedora|centos|rhel|ol|sles|opensuse*) - PACKAGE_TYPE="rpm" - ;; - alpine) - PACKAGE_TYPE="apk" - ;; - *) - if command_exists apt-get || command_exists dpkg; then - PACKAGE_TYPE="deb" - elif command_exists yum || command_exists dnf || command_exists zypper; then - PACKAGE_TYPE="rpm" - elif command_exists apk; then - PACKAGE_TYPE="apk" - else - PACKAGE_TYPE="other" - fi - ;; - esac - export PACKAGE_TYPE - echo "# Detected package type: $PACKAGE_TYPE" -} - -# Init function -init() { - # Detect basic distribution info - get_distribution - - # Set up sudo for privileged commands - setup_sudo - - # Refine version information - refine_distribution_version - - # Check if this is a forked distro - check_forked - - # Detect init system and package type (for universal OS/init support) - detect_init_system - detect_package_type - - # Print final distribution information - echo "----------------------------------------" - echo "Linux Distribution: $lsb_dist" - echo "Version: $dist_version" - echo "Init system: $INIT_SYSTEM" - echo "Package type: $PACKAGE_TYPE" - echo "----------------------------------------" - -} diff --git a/assets/agent/install_container_engine.sh b/assets/agent/install_container_engine.sh deleted file mode 100755 index e05046198..000000000 --- a/assets/agent/install_container_engine.sh +++ /dev/null @@ -1,284 +0,0 @@ -#!/bin/sh -# Script to install Docker/Podman based on Linux distribution -# Sources init.sh for distribution detection - -set -x -set -e - -CONTAINER_ENGINE_MSG="This operating system does not support automatic container engine installation. Please install Docker 25+ or Podman 4+ on the target host and re-run, or use an airgap deployment with a pre-installed engine." - -# Check Docker version (need >= 25). Sets docker_version_num for comparison. -check_docker_version() { - docker_version_num=0 - if command -v docker >/dev/null 2>&1; then - raw=$(docker -v 2>/dev/null | sed 's/.*version \([^,]*\),.*/\1/' | tr -d '.') - [ -n "$raw" ] && docker_version_num="$raw" - fi - [ "$docker_version_num" -ge 2500 ] 2>/dev/null || return 1 -} - -# Check Podman version (need >= 4). Sets podman_version_num for comparison. -check_podman_version() { - podman_version_num=0 - if command -v podman >/dev/null 2>&1; then - raw=$(podman --version 2>/dev/null | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p') - [ -n "$raw" ] && podman_version_num="$raw" - fi - [ "$podman_version_num" -ge 4 ] 2>/dev/null || return 1 -} - -# Start Docker daemon (init-aware) -start_docker() { - set +e - if $sh_c "docker ps" >/dev/null 2>&1; then - set -e - return 0 - fi - err_code=1 - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl start docker" >/dev/null 2>&1 - err_code=$? - ;; - sysvinit) - $sh_c "service docker start" >/dev/null 2>&1 || $sh_c "/etc/init.d/docker start" >/dev/null 2>&1 - err_code=$? - ;; - openrc) - $sh_c "rc-service docker start" >/dev/null 2>&1 - err_code=$? - ;; - *) - $sh_c "/etc/init.d/docker start" >/dev/null 2>&1 - err_code=$? - [ $err_code -ne 0 ] && $sh_c "systemctl start docker" >/dev/null 2>&1 && err_code=0 - [ $err_code -ne 0 ] && $sh_c "service docker start" >/dev/null 2>&1 && err_code=0 - [ $err_code -ne 0 ] && $sh_c "snap start docker" >/dev/null 2>&1 && err_code=0 - ;; - esac - set -e - if [ $err_code -ne 0 ]; then - echo "Could not start Docker daemon" - exit 1 - fi -} - -# Start Podman (init-aware) -start_podman() { - set +e - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl start podman" >/dev/null 2>&1 - $sh_c "systemctl start podman.socket" >/dev/null 2>&1 - ;; - sysvinit) - $sh_c "service podman start" >/dev/null 2>&1 || $sh_c "/etc/init.d/podman start" >/dev/null 2>&1 - ;; - openrc) - $sh_c "rc-service podman start" >/dev/null 2>&1 - ;; - *) - $sh_c "systemctl start podman" >/dev/null 2>&1 || true - $sh_c "systemctl start podman.socket" >/dev/null 2>&1 || true - $sh_c "service podman start" >/dev/null 2>&1 || true - ;; - esac - set -e -} - - -do_modify_daemon() { - # Skip for Podman installations - if [ "$USE_PODMAN" = "true" ]; then - echo "# Configuring Podman for CDI directory support..." - - # Create CDI directories - $sh_c "mkdir -p /etc/cdi /var/run/cdi" - - # Ensure /etc/containers exists - $sh_c "mkdir -p /etc/containers" - - # Create containers.conf if it doesn't exist - if [ ! -f "/etc/containers/containers.conf" ]; then - $sh_c 'cat > /etc/containers/containers.conf <> /etc/containers/containers.conf' - fi - fi - - # Enable and start Podman (init-aware) - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl enable podman" 2>/dev/null || true - $sh_c "systemctl enable podman.socket" 2>/dev/null || true - ;; - openrc) - $sh_c "rc-update add podman default" 2>/dev/null || true - ;; - sysvinit) - $sh_c "update-rc.d podman defaults" 2>/dev/null || $sh_c "chkconfig podman on" 2>/dev/null || true - ;; - *) ;; - esac - start_podman - return - fi - - # Original Docker daemon configuration - if [ ! -f /etc/docker/daemon.json ]; then - echo "Creating /etc/docker/daemon.json..." - $sh_c "mkdir -p /etc/docker" - $sh_c 'cat > /etc/docker/daemon.json << EOF -{ - "storage-driver": "overlayfs", - "features": { - "containerd-snapshotter": true, - "cdi": true - }, - "cdi-spec-dirs": ["/etc/cdi/", "/var/run/cdi"] -} -EOF' - else - echo "/etc/docker/daemon.json already exists" - fi - echo "Restarting Docker daemon..." - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl daemon-reload" - $sh_c "systemctl restart docker" - ;; - *) - $sh_c "systemctl daemon-reload" 2>/dev/null || true - $sh_c "systemctl restart docker" 2>/dev/null || start_docker - ;; - esac -} - -do_install_container_engine() { - # PACKAGE_TYPE other: only check engine presence/version, do not install - if [ "$PACKAGE_TYPE" = "other" ]; then - if check_docker_version; then - USE_PODMAN="false" - echo "# Docker (>= 25) found; using Docker." - start_docker - do_modify_daemon - return 0 - fi - if check_podman_version; then - USE_PODMAN="true" - echo "# Podman (>= 4) found; using Podman." - do_modify_daemon - return 0 - fi - echo "Error: $CONTAINER_ENGINE_MSG" - exit 1 - fi - - if [ "$USE_PODMAN" = "true" ]; then - echo "# Installing Podman and related packages..." - case "$lsb_dist" in - fedora|centos|rhel|ol) - $sh_c "yum install -y podman crun podman-docker" - ;; - sles|opensuse*) - $sh_c "zypper install -y podman crun podman-docker" - ;; - esac - if ! check_podman_version; then - echo "Error: Podman 4+ is required. Please upgrade Podman." - exit 1 - fi - do_modify_daemon - return - fi - - # Docker: check existing version first - if command_exists docker; then - docker_version=$(docker -v 2>/dev/null | sed 's/.*version \([^,]*\),.*/\1/' | tr -d '.') - if [ -n "$docker_version" ] && [ "$docker_version" -ge 2500 ] 2>/dev/null; then - echo "# Docker already installed (>= 25)" - start_docker - do_modify_daemon - return - fi - fi - - echo "# Installing Docker..." - case "$lsb_dist" in - debian|ubuntu|raspbian) - case "$dist_version" in - "stretch") - $sh_c "apt install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common" - curl -fsSL https://download.docker.com/linux/debian/gpg | $sh_c "apt-key add -" - $sh_c "add-apt-repository \"deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/debian $(lsb_release -cs) stable\"" - $sh_c "apt update -y" - $sh_c "apt install -y docker-ce" - ;; - *) - curl -fsSL https://get.docker.com/ | $sh_c "sh" - ;; - esac - ;; - *) - curl -fsSL https://get.docker.com/ | $sh_c "sh" - ;; - esac - - if ! command_exists docker; then - echo "Failed to install Docker" - exit 1 - fi - if ! check_docker_version; then - echo "Error: Docker 25+ is required. Please upgrade Docker." - exit 1 - fi - start_docker - do_modify_daemon -} - -# Check if we should use Podman based on distribution -determine_container_engine() { - USE_PODMAN="false" - case "$lsb_dist" in - fedora|centos|rhel|ol|sles|opensuse*) - USE_PODMAN="true" - echo "# Using Podman for $lsb_dist" - ;; - *) - echo "# Using Docker for $lsb_dist" - ;; - esac -} - -# Source init.sh to get distribution info -. /etc/iofog/agent/init.sh -init - -# Configure container engine based on distribution -determine_container_engine - -# Install appropriate container engine -do_install_container_engine - -echo "# Installation completed successfully" \ No newline at end of file diff --git a/assets/agent/install_deps.sh b/assets/agent/install_deps.sh deleted file mode 100755 index 0e48acc3d..000000000 --- a/assets/agent/install_deps.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -x -set -e - -/etc/iofog/agent/install_java.sh -/etc/iofog/agent/install_container_engine.sh diff --git a/assets/agent/install_iofog.sh b/assets/agent/install_iofog.sh deleted file mode 100755 index d01c92b89..000000000 --- a/assets/agent/install_iofog.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/sh -set -x -set -e - -do_check_install() { - if command_exists iofog-agent; then - local VERSION=$(sudo iofog-agent version | head -n1 | sed "s/ioFog//g" | tr -d ' ' | tr -d "\n") - if [ "$VERSION" = "$agent_version" ]; then - echo "Agent $VERSION already installed." - exit 0 - fi - fi -} - -do_stop_iofog() { - if command_exists iofog-agent; then - sudo systemctl stop iofog-agent - fi -} - - - -do_install_iofog() { - AGENT_CONFIG_FOLDER=/etc/iofog-agent - SAVED_AGENT_CONFIG_FOLDER=/tmp/agent-config-save - echo "# Installing ioFog agent..." - - # Save iofog-agent config - if [ -d ${AGENT_CONFIG_FOLDER} ]; then - sudo rm -rf ${SAVED_AGENT_CONFIG_FOLDER} - sudo mkdir -p ${SAVED_AGENT_CONFIG_FOLDER} - sudo cp -r ${AGENT_CONFIG_FOLDER}/* ${SAVED_AGENT_CONFIG_FOLDER}/ - fi - - echo $lsb_dist - case "$lsb_dist" in - fedora|rhel|ol|centos) - $sh_c "yum update -y" - $sh_c "yum install -y iofog-agent-$agent_version-1.noarch" - ;; - sles|opensuse) - $sh_c "zypper refresh" - $sh_c "zypper install -y iofog-agent=$agent_version" - ;; - *) - $sh_c "apt update -qy" - $sh_c "apt install --allow-downgrades iofog-agent=$agent_version -qy" - ;; - esac - - # Restore iofog-agent config - if [ -d ${SAVED_AGENT_CONFIG_FOLDER} ]; then - sudo mv ${SAVED_AGENT_CONFIG_FOLDER}/* ${AGENT_CONFIG_FOLDER}/ - sudo rmdir ${SAVED_AGENT_CONFIG_FOLDER} - fi - sudo chmod 775 ${AGENT_CONFIG_FOLDER} -} - -do_start_iofog(){ - - sudo systemctl start iofog-agent > /dev/null 2&>1 & - local STATUS="" - local ITER=0 - while [ "$STATUS" != "RUNNING" ] ; do - ITER=$((ITER+1)) - if [ "$ITER" -gt 600 ]; then - echo 'Timed out waiting for Agent to be RUNNING' - exit 1; - fi - sleep 1 - STATUS=$(sudo iofog-agent status | cut -f2 -d: | head -n 1 | tr -d '[:space:]') - echo "${STATUS}" - done - sudo iofog-agent "config -cf 10 -sf 10" - if [ "$lsb_dist" = "rhel" ] || [ "$lsb_dist" = "centos" ] || [ "$lsb_dist" = "fedora" ] || [ "$lsb_dist" = "ol" ] || [ "$lsb_dist" = "sles" ] || [ "$lsb_dist" = "opensuse" ]; then - sudo iofog-agent "config -c unix:///var/run/podman/podman.sock" - fi -} - -agent_version="$1" -echo "Using variables" -echo "version: $agent_version" - -. /etc/iofog/agent/init.sh -init - -# Native agent is supported only on package-managed OSes (deb/rpm) with systemd -if [ "$PACKAGE_TYPE" != "deb" ] && [ "$PACKAGE_TYPE" != "rpm" ]; then - echo "Error: This operating system is not supported for native agent installation." - echo "Please deploy the agent as a container (container agent) on this host." - exit 1 -fi -if [ "$INIT_SYSTEM" != "systemd" ]; then - echo "Error: Native agent is supported only on systemd. This system uses $INIT_SYSTEM." - echo "Please deploy the agent as a container (container agent) on this host." - exit 1 -fi - -do_check_install -do_stop_iofog -do_install_iofog -do_start_iofog \ No newline at end of file diff --git a/assets/agent/install_java.sh b/assets/agent/install_java.sh deleted file mode 100755 index dc76ee948..000000000 --- a/assets/agent/install_java.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/sh -set -x -set -e - -java_major_version=0 -java_minor_version=0 -do_check_install() { - if command_exists java; then - java_major_version="$(java --version | head -n1 | awk '{print $2}' | cut -d. -f1)" - java_minor_version="$(java --version | head -n1 | awk '{print $2}' | cut -d. -f2)" - fi - if [ "$java_major_version" -ge "17" ] && [ "$java_minor_version" -ge "0" ]; then - echo "Java $java_major_version.$java_minor_version already installed." - exit 0 - fi -} - -do_install_java() { - echo "# Installing java 17..." - echo "" - os_arch=$(getconf LONG_BIT) - is_arm="" - if [ "$lsb_dist" = "raspbian" ] || [ "$(uname -m)" = "armv7l" ] || [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "armv8" ]; then - is_arm="-arm" - fi - case "$lsb_dist" in - ubuntu|debian|raspbian|mendel) - $sh_c "apt-get update -y" - $sh_c "apt install -y openjdk-17-jdk" - ;; - fedora|centos|rhel|ol) - $sh_c "yum install -y java-17-openjdk" - ;; - sles|opensuse*) - $sh_c "zypper refresh" - $sh_c "zypper install -y java-17-openjdk" - ;; - *) - echo "Unsupported distribution: $lsb_dist" - exit 1 - ;; - esac -} - -do_install_deps() { - local installer="" - case "$lsb_dist" in - ubuntu|debian|raspbian|mendel) - installer="apt" - ;; - fedora|centos|rhel|ol) - installer="yum" - ;; - sles|opensuse*) - installer="zypper" - ;; - *) - echo "Unsupported distribution: $lsb_dist" - exit 1 - ;; - esac - - local iter=0 - while ! $sh_c "$installer update" && [ "$iter" -lt 6 ]; do - sleep 5 - iter=$((iter+1)) - done -} - -. /etc/iofog/agent/init.sh -init -do_check_install -do_install_deps -do_install_java \ No newline at end of file diff --git a/assets/agent/uninstall_iofog.sh b/assets/agent/uninstall_iofog.sh deleted file mode 100755 index 598f094b6..000000000 --- a/assets/agent/uninstall_iofog.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh -set -x -set -e - -AGENT_CONFIG_FOLDER=/etc/iofog-agent/ -AGENT_LOG_FOLDER=/var/log/iofog-agent/ - -do_uninstall_iofog() { - echo "# Removing ioFog agent..." - - case "$lsb_dist" in - ubuntu|debian|raspbian) - $sh_c "apt-get -y --purge autoremove iofog-agent" - ;; - fedora|centos|rhel|ol) - $sh_c "yum remove -y iofog-agent" - ;; - sles|opensuse) - $sh_c "zypper remove -y iofog-agent" - ;; - *) - echo "Error: Unsupported Linux distribution: $lsb_dist" - exit 1 - ;; - esac - - # Remove config files - $sh_c "rm -rf ${AGENT_CONFIG_FOLDER}" - - # Remove log files - $sh_c "rm -rf ${AGENT_LOG_FOLDER}" -} - -. /etc/iofog/agent/init.sh -init - -do_uninstall_iofog \ No newline at end of file diff --git a/assets/airgap-agent/check_prereqs.sh b/assets/airgap-agent/check_prereqs.sh deleted file mode 100755 index a6b148d4a..000000000 --- a/assets/airgap-agent/check_prereqs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -x - -# Check can sudo without password -if ! $(sudo ls /tmp/ > /dev/null); then - MSG="Unable to successfully use sudo with user $USER on this host.\nUser $USER must be in sudoers group and using sudo without password must be enabled.\nPlease see iofog.org documentation for more details." - echo $MSG - exit 1 -fi \ No newline at end of file diff --git a/assets/airgap-agent/init.sh b/assets/airgap-agent/init.sh deleted file mode 100755 index 630898c81..000000000 --- a/assets/airgap-agent/init.sh +++ /dev/null @@ -1,296 +0,0 @@ -#!/bin/sh -# Script to detect Linux distribution and version -# Used as a precursor for system-specific installations - -# Exit on error and print commands for debugging -set -e -set -x - -# Define user variable -user="$(id -un 2>/dev/null || true)" - -# Check if a command exists -command_exists() { - command -v "$@" > /dev/null 2>&1 -} - -# Detect the Linux distribution -get_distribution() { - lsb_dist="" - dist_version="" - - # Every system that we officially support has /etc/os-release - if [ -r /etc/os-release ]; then - - lsb_dist="$(. /etc/os-release && echo "$ID")" - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')" - else - echo "Error: Unsupported Linux distribution! /etc/os-release not found." - exit 1 - fi - - echo "# Detected distribution: $lsb_dist (version: $dist_version)" -} - -# Check if this is a forked Linux distro -check_forked() { - # Skip if lsb_release doesn't exist - if ! command_exists lsb_release; then - return - fi - - # Check if the `-u` option is supported - set +e - lsb_release -a > /dev/null 2>&1 - lsb_release_exit_code=$? - set -e - - # Check if the command has exited successfully, it means we're in a forked distro - if [ "$lsb_release_exit_code" = "0" ]; then - # Get the upstream release info - current_lsb_dist=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]') - current_dist_version=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]') - - # Print info about current distro - echo "You're using '$current_lsb_dist' version '$current_dist_version'." - - # Check if current is different from detected (indicating a fork) - if [ "$current_lsb_dist" != "$lsb_dist" ] || [ "$current_dist_version" != "$dist_version" ]; then - echo "Upstream release is '$lsb_dist' version '$dist_version'." - fi - else - # Additional checks for specific distros that might not be properly detected - if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then - if [ "$lsb_dist" = "osmc" ]; then - # OSMC runs Raspbian - lsb_dist=raspbian - else - # We're Debian and don't even know it! - lsb_dist=debian - fi - # Get Debian version and map it to codename - dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" - case "$dist_version" in - 14) - dist_version="forky" - ;; - 13) - dist_version="trixie" - ;; - 12) - dist_version="bookworm" - ;; - 11) - dist_version="bullseye" - ;; - 10) - dist_version="buster" - ;; - 9) - dist_version="stretch" - ;; - 8|'Kali Linux 2') - dist_version="jessie" - ;; - 7) - dist_version="wheezy" - ;; - esac - elif [ -r /etc/redhat-release ] && [ -z "$lsb_dist" ]; then - lsb_dist=redhat - # Extract version from redhat-release file - dist_version="$(sed 's/.*release \([0-9.]*\).*/\1/' /etc/redhat-release)" - fi - fi -} - -# Set up sudo command if necessary -setup_sudo() { - sh_c='sh -c' - if [ "$user" != 'root' ]; then - if command_exists sudo; then - sh_c='sudo -E sh -c' - elif command_exists su; then - sh_c='su -c' - else - echo "Error: this installer needs the ability to run commands as root." - echo "We are unable to find either 'sudo' or 'su' available to make this happen." - exit 1 - fi - fi - echo "# Using command executor: $sh_c" -} - -# Refine distribution version detection based on the distro -refine_distribution_version() { - case "$lsb_dist" in - ubuntu) - if command_exists lsb_release; then - dist_version="$(lsb_release --codename | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then - - dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")" - fi - ;; - - debian|raspbian) - # If we only have a number, map it to a codename for better recognition - if echo "$dist_version" | grep -qE '^[0-9]+$'; then - case "$dist_version" in - 14) - dist_version="forky" - ;; - 13) - dist_version="trixie" - ;; - 12) - dist_version="bookworm" - ;; - 11) - dist_version="bullseye" - ;; - 10) - # Handle special case for Buster - dist_version="buster" - if [ "$user" = 'root' ]; then - apt-get update --allow-releaseinfo-change || true - elif command_exists sudo; then - sudo apt-get update --allow-releaseinfo-change || true - fi - ;; - 9) - dist_version="stretch" - ;; - 8) - dist_version="jessie" - ;; - 7) - dist_version="wheezy" - ;; - esac - fi - ;; - - centos|rhel|fedora|ol) - # Make sure we have a version number - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - if [ -z "$dist_version" ] && [ -r /etc/redhat-release ]; then - dist_version="$(sed 's/.*release \([0-9.]*\).*/\1/' /etc/redhat-release)" - fi - ;; - - sles|opensuse) - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - # Fallback for older versions - if [ -z "$dist_version" ] && [ -r /etc/SuSE-release ]; then - dist_version="$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = //')" - fi - # Ensure version is in the correct format (e.g., 15.4 for SLES 15 SP4) - if [ -n "$dist_version" ]; then - # Remove any non-numeric characters except dots - dist_version="$(echo "$dist_version" | sed 's/[^0-9.]//g')" - fi - # Normalize distribution name - if [ "$lsb_dist" = "sles" ]; then - lsb_dist="sles" - elif [ "$lsb_dist" = "opensuse" ]; then - lsb_dist="opensuse" - fi - ;; - - *) - if command_exists lsb_release; then - dist_version="$(lsb_release --release | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - ;; - esac -} - -# Detect init system -detect_init_system() { - if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then - INIT_SYSTEM="systemd" - elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then - INIT_SYSTEM="upstart" - elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then - INIT_SYSTEM="openrc" - elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then - INIT_SYSTEM="s6" - elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then - INIT_SYSTEM="runit" - elif [ -d /etc/init.d ]; then - INIT_SYSTEM="sysvinit" - else - INIT_SYSTEM="unknown" - fi - export INIT_SYSTEM - echo "# Detected init system: $INIT_SYSTEM" -} - -# Detect package type (deb, rpm, or other) -detect_package_type() { - case "$lsb_dist" in - debian|ubuntu|raspbian|mendel) - PACKAGE_TYPE="deb" - ;; - fedora|centos|rhel|ol|sles|opensuse*) - PACKAGE_TYPE="rpm" - ;; - alpine) - PACKAGE_TYPE="apk" - ;; - *) - if command_exists apt-get || command_exists dpkg; then - PACKAGE_TYPE="deb" - elif command_exists yum || command_exists dnf || command_exists zypper; then - PACKAGE_TYPE="rpm" - elif command_exists apk; then - PACKAGE_TYPE="apk" - else - PACKAGE_TYPE="other" - fi - ;; - esac - export PACKAGE_TYPE - echo "# Detected package type: $PACKAGE_TYPE" -} - -# Init function -init() { - # Detect basic distribution info - get_distribution - - # Set up sudo for privileged commands - setup_sudo - - # Refine version information - refine_distribution_version - - # Check if this is a forked distro - check_forked - - # Detect init system and package type (for universal OS/init support) - detect_init_system - detect_package_type - - # Print final distribution information - echo "----------------------------------------" - echo "Linux Distribution: $lsb_dist" - echo "Version: $dist_version" - echo "Init system: $INIT_SYSTEM" - echo "Package type: $PACKAGE_TYPE" - echo "----------------------------------------" - -} diff --git a/assets/airgap-agent/install_container_engine.sh b/assets/airgap-agent/install_container_engine.sh deleted file mode 100644 index 3e04753d2..000000000 --- a/assets/airgap-agent/install_container_engine.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/bin/sh -# Script to configure Docker/Podman for airgap deployment -# Check-only: verifies container engine is installed (Docker 25+ or Podman 4+), then configures/starts -# Sources init.sh for distribution detection - -set -x -set -e - -CONTAINER_ENGINE_MSG="This operating system does not support automatic container engine installation. Please install Docker 25+ or Podman 4+ on the target host and re-run, or use an airgap deployment with a pre-installed engine." - -check_docker_version() { - docker_version_num=0 - if command -v docker >/dev/null 2>&1; then - raw=$(docker -v 2>/dev/null | sed 's/.*version \([^,]*\),.*/\1/' | tr -d '.') - [ -n "$raw" ] && docker_version_num="$raw" - fi - [ "$docker_version_num" -ge 2500 ] 2>/dev/null || return 1 -} - -check_podman_version() { - podman_version_num=0 - if command -v podman >/dev/null 2>&1; then - raw=$(podman --version 2>/dev/null | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p') - [ -n "$raw" ] && podman_version_num="$raw" - fi - [ "$podman_version_num" -ge 4 ] 2>/dev/null || return 1 -} - -start_docker() { - set +e - if $sh_c "docker ps" >/dev/null 2>&1; then - set -e - return 0 - fi - err_code=1 - case "${INIT_SYSTEM:-unknown}" in - systemd) $sh_c "systemctl start docker" >/dev/null 2>&1; err_code=$? ;; - sysvinit) $sh_c "service docker start" >/dev/null 2>&1 || $sh_c "/etc/init.d/docker start" >/dev/null 2>&1; err_code=$? ;; - openrc) $sh_c "rc-service docker start" >/dev/null 2>&1; err_code=$? ;; - *) - $sh_c "/etc/init.d/docker start" >/dev/null 2>&1 - err_code=$? - [ $err_code -ne 0 ] && $sh_c "systemctl start docker" >/dev/null 2>&1 && err_code=0 - [ $err_code -ne 0 ] && $sh_c "service docker start" >/dev/null 2>&1 && err_code=0 - [ $err_code -ne 0 ] && $sh_c "snap start docker" >/dev/null 2>&1 && err_code=0 - ;; - esac - set -e - if [ $err_code -ne 0 ]; then - echo "Could not start Docker daemon" - exit 1 - fi -} - -start_podman() { - set +e - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl start podman" >/dev/null 2>&1 - $sh_c "systemctl start podman.socket" >/dev/null 2>&1 - ;; - sysvinit) $sh_c "service podman start" >/dev/null 2>&1 || $sh_c "/etc/init.d/podman start" >/dev/null 2>&1 ;; - openrc) $sh_c "rc-service podman start" >/dev/null 2>&1 ;; - *) - $sh_c "systemctl start podman" >/dev/null 2>&1 || true - $sh_c "systemctl start podman.socket" >/dev/null 2>&1 || true - $sh_c "service podman start" >/dev/null 2>&1 || true - ;; - esac - set -e -} - -do_modify_daemon() { - # Skip for Podman installations - if [ "$USE_PODMAN" = "true" ]; then - echo "# Configuring Podman for CDI directory support..." - - # Create CDI directories - $sh_c "mkdir -p /etc/cdi /var/run/cdi" - - # Ensure /etc/containers exists - $sh_c "mkdir -p /etc/containers" - - # Create containers.conf if it doesn't exist - if [ ! -f "/etc/containers/containers.conf" ]; then - $sh_c 'cat > /etc/containers/containers.conf <> /etc/containers/containers.conf' - fi - fi - - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl enable podman" 2>/dev/null || true - $sh_c "systemctl enable podman.socket" 2>/dev/null || true - ;; - openrc) $sh_c "rc-update add podman default" 2>/dev/null || true ;; - sysvinit) $sh_c "update-rc.d podman defaults" 2>/dev/null || $sh_c "chkconfig podman on" 2>/dev/null || true ;; - *) ;; - esac - start_podman - return - fi - - # Original Docker daemon configuration - if [ ! -f /etc/docker/daemon.json ]; then - echo "Creating /etc/docker/daemon.json..." - $sh_c "mkdir -p /etc/docker" - $sh_c 'cat > /etc/docker/daemon.json << EOF -{ - "storage-driver": "overlayfs", - "features": { - "containerd-snapshotter": true, - "cdi": true - }, - "cdi-spec-dirs": ["/etc/cdi/", "/var/run/cdi"] -} -EOF' - else - echo "/etc/docker/daemon.json already exists" - fi - echo "Restarting Docker daemon..." - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl daemon-reload" - $sh_c "systemctl restart docker" - ;; - *) - $sh_c "systemctl daemon-reload" 2>/dev/null || true - $sh_c "systemctl restart docker" 2>/dev/null || start_docker - ;; - esac -} - -# Airgap: determine engine by availability (Docker 25+ or Podman 4+) -determine_container_engine() { - if check_docker_version; then - USE_PODMAN="false" - echo "# Using Docker (25+)" - elif check_podman_version; then - USE_PODMAN="true" - echo "# Using Podman (4+)" - else - echo "Error: Docker 25+ or Podman 4+ is required. $CONTAINER_ENGINE_MSG" - exit 1 - fi -} - -. /etc/iofog/agent/init.sh -init - -determine_container_engine - -# Start engine and configure -if [ "$USE_PODMAN" = "false" ]; then - start_docker -fi - -do_modify_daemon - -echo "# Container engine configuration completed successfully" - diff --git a/assets/airgap-agent/install_deps.sh b/assets/airgap-agent/install_deps.sh deleted file mode 100755 index ab04c7f99..000000000 --- a/assets/airgap-agent/install_deps.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -x -set -e - - -/etc/iofog/agent/install_container_engine.sh diff --git a/assets/airgap-agent/install_iofog.sh b/assets/airgap-agent/install_iofog.sh deleted file mode 100644 index 1273d1e4a..000000000 --- a/assets/airgap-agent/install_iofog.sh +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -set -x -set -e - -AGENT_LOG_FOLDER=/var/log/iofog-agent -AGENT_BACKUP_FOLDER=/var/backups/iofog-agent -AGENT_MESSAGE_FOLDER=/var/lib/iofog-agent -AGENT_SHARE_FOLDER=/usr/share/iofog-agent -SAVED_AGENT_CONFIG_FOLDER=/tmp/agent-config-save -AGENT_CONTAINER_NAME="iofog-agent" -ETC_DIR=/etc/iofog/agent - -do_check_install() { - if command_exists iofog-agent; then - local VERSION=$(sudo iofog-agent version | head -n1 | sed "s/ioFog//g" | tr -d ' ' | tr -d "\n") - if [ "$VERSION" = "$agent_version" ]; then - echo "Agent $VERSION already installed." - exit 0 - fi - fi -} - -do_stop_iofog() { - if ! command_exists iofog-agent; then - return 0 - fi - case "${INIT_SYSTEM:-systemd}" in - systemd) - sudo systemctl stop iofog-agent 2>/dev/null || true - ;; - sysvinit|openrc) - sudo service iofog-agent stop 2>/dev/null || sudo /etc/init.d/iofog-agent stop 2>/dev/null || true - ;; - s6) - sudo s6-svc -d /etc/s6/sv/iofog-agent 2>/dev/null || true - ;; - runit) - sudo sv stop iofog-agent 2>/dev/null || true - ;; - upstart) - sudo initctl stop iofog-agent 2>/dev/null || true - ;; - *) - sudo systemctl stop iofog-agent 2>/dev/null || sudo service iofog-agent stop 2>/dev/null || true - ;; - esac - (docker stop ${AGENT_CONTAINER_NAME} 2>/dev/null || podman stop ${AGENT_CONTAINER_NAME} 2>/dev/null) || true -} - -do_create_env() { -ENV_FILE_NAME=iofog-agent.env # Used as an env file in systemd - -ENV_FILE="$ETC_DIR/$ENV_FILE_NAME" - -# Env file (for systemd) -rm -f "$ENV_FILE" -touch "$ENV_FILE" - -echo "IOFOG_AGENT_IMAGE=${agent_image}" >> "$ENV_FILE" -echo "IOFOG_AGENT_TZ=${agent_tz}" >> "$ENV_FILE" - -} - -do_install_iofog() { - echo "# Installing ioFog agent (airgap mode)..." - - for FOLDER in ${ETC_DIR} ${AGENT_LOG_FOLDER} ${AGENT_BACKUP_FOLDER} ${AGENT_MESSAGE_FOLDER} ${AGENT_SHARE_FOLDER}; do - if [ ! -d "$FOLDER" ]; then - echo "Creating folder: $FOLDER" - sudo mkdir -p "$FOLDER" - sudo chmod 775 "$FOLDER" - fi - done - do_create_env - - USE_PODMAN="false" - case "$lsb_dist" in - rhel|centos|fedora|ol|sles|opensuse*) USE_PODMAN="true" ;; - esac - if [ "$USE_PODMAN" = "true" ]; then - CONTAINER_RUNTIME="podman" - SOCK_MOUNT="-v /run/podman/podman.sock:/run/podman/podman.sock:rw" - else - CONTAINER_RUNTIME="docker" - SOCK_MOUNT="-v /var/run/docker.sock:/var/run/docker.sock:rw" - fi - - if [ "${INIT_SYSTEM:-systemd}" = "systemd" ]; then - if [ "$USE_PODMAN" = "true" ]; then - echo "Using Podman (Quadlet) for container management..." - sudo mkdir -p /etc/containers/systemd - cat < /dev/null -[Unit] -Description=ioFog Agent Service -After=podman.service -Requires=podman.service - -[Container] -ContainerName=${AGENT_CONTAINER_NAME} -Image=${agent_image} -PodmanArgs=--privileged --stop-timeout=60 -EnvironmentFile=${ETC_DIR}/iofog-agent.env -Network=host -Volume=/run/podman/podman.sock:/run/podman/podman.sock:rw -Volume=iofog-agent-config:/etc/iofog-agent:rw -Volume=/var/log/iofog-agent:/var/log/iofog-agent:rw -Volume=/var/backups/iofog-agent:/var/backups/iofog-agent:rw -Volume=/usr/share/iofog-agent:/usr/share/iofog-agent:rw -Volume=/var/lib/iofog-agent:/var/lib/iofog-agent:rw -LogDriver=journald - -[Service] -Restart=always - -[Install] -WantedBy=default.target -EOF - sudo systemctl daemon-reload - sudo systemctl restart podman 2>/dev/null || true - sudo systemctl enable iofog-agent.service - sudo systemctl start iofog-agent.service - else - echo "Using Docker (systemd) for container management..." - cat < /dev/null -[Unit] -Description=ioFog Agent Service -After=docker.service -Requires=docker.service - -[Service] -Restart=always -ExecStartPre=-/usr/bin/docker rm -f ${AGENT_CONTAINER_NAME} -ExecStart=/usr/bin/docker run --rm --name ${AGENT_CONTAINER_NAME} \\ ---env-file ${ETC_DIR}/iofog-agent.env \\ --v /var/run/docker.sock:/var/run/docker.sock:rw \\ --v iofog-agent-config:/etc/iofog-agent:rw \\ --v /var/log/iofog-agent:/var/log/iofog-agent:rw \\ --v /var/backups/iofog-agent:/var/backups/iofog-agent:rw \\ --v /usr/share/iofog-agent:/usr/share/iofog-agent:rw \\ --v /var/lib/iofog-agent:/var/lib/iofog-agent:rw \\ ---net=host \\ ---privileged \\ ---stop-timeout 60 \\ ---attach stdout \\ ---attach stderr \\ -${agent_image} -ExecStop=/usr/bin/docker stop ${AGENT_CONTAINER_NAME} - -[Install] -WantedBy=default.target -EOF - sudo systemctl daemon-reload - sudo systemctl enable iofog-agent.service - sudo systemctl start iofog-agent.service - fi - else - echo "Using $CONTAINER_RUNTIME with $INIT_SYSTEM for container management..." - RUN_CMD="${CONTAINER_RUNTIME} run --rm -d --name ${AGENT_CONTAINER_NAME} --env-file ${ETC_DIR}/iofog-agent.env ${SOCK_MOUNT} -v iofog-agent-config:/etc/iofog-agent:rw -v /var/log/iofog-agent:/var/log/iofog-agent:rw -v /var/backups/iofog-agent:/var/backups/iofog-agent:rw -v /usr/share/iofog-agent:/usr/share/iofog-agent:rw -v /var/lib/iofog-agent:/var/lib/iofog-agent:rw --net=host --privileged --stop-timeout 60 ${agent_image}" - RUN_CMD_FG="${CONTAINER_RUNTIME} run --rm --name ${AGENT_CONTAINER_NAME} --env-file ${ETC_DIR}/iofog-agent.env ${SOCK_MOUNT} -v iofog-agent-config:/etc/iofog-agent:rw -v /var/log/iofog-agent:/var/log/iofog-agent:rw -v /var/backups/iofog-agent:/var/backups/iofog-agent:rw -v /usr/share/iofog-agent:/usr/share/iofog-agent:rw -v /var/lib/iofog-agent:/var/lib/iofog-agent:rw --net=host --privileged --stop-timeout 60 ${agent_image}" - STOP_CMD="${CONTAINER_RUNTIME} stop ${AGENT_CONTAINER_NAME}" - case "$INIT_SYSTEM" in - sysvinit|openrc) - sudo tee /etc/init.d/iofog-agent > /dev/null </dev/null | grep -q "^${AGENT_CONTAINER_NAME}\$"; then exit 0; fi - $RUN_CMD - ;; - stop) $STOP_CMD 2>/dev/null || true ;; - restart) \$0 stop; \$0 start ;; - status) - if ${CONTAINER_RUNTIME} ps --format '{{.Names}}' 2>/dev/null | grep -q "^${AGENT_CONTAINER_NAME}\$"; then echo "running"; exit 0; else echo "stopped"; exit 1; fi - ;; - *) echo "Usage: \$0 {start|stop|restart|status}"; exit 1 ;; -esac -exit 0 -INITSCRIPT - sudo chmod +x /etc/init.d/iofog-agent - if [ "$INIT_SYSTEM" = "openrc" ]; then - sudo rc-update add iofog-agent default 2>/dev/null || true - sudo rc-service iofog-agent start - else - sudo update-rc.d iofog-agent defaults 2>/dev/null || sudo chkconfig iofog-agent on 2>/dev/null || true - sudo service iofog-agent start 2>/dev/null || sudo /etc/init.d/iofog-agent start - fi - ;; - s6) - sudo mkdir -p /etc/s6/sv/iofog-agent - printf '#!/bin/sh\nexec %s\n' "$RUN_CMD_FG" | sudo tee /etc/s6/sv/iofog-agent/run > /dev/null - sudo chmod +x /etc/s6/sv/iofog-agent/run - [ -d /etc/s6/adminsv/default ] && sudo ln -sf /etc/s6/sv/iofog-agent /etc/s6/adminsv/default/iofog-agent 2>/dev/null || true - sudo s6-svc -u /etc/s6/sv/iofog-agent 2>/dev/null || true - ;; - runit) - sudo mkdir -p /etc/runit/sv/iofog-agent - printf '#!/bin/sh\nexec %s\n' "$RUN_CMD_FG" | sudo tee /etc/runit/sv/iofog-agent/run > /dev/null - sudo chmod +x /etc/runit/sv/iofog-agent/run - [ -d /var/service ] && sudo ln -sf /etc/runit/sv/iofog-agent /var/service/iofog-agent 2>/dev/null || true - [ -d /etc/runit/runsvdir/default ] && sudo ln -sf /etc/runit/sv/iofog-agent /etc/runit/runsvdir/default/iofog-agent 2>/dev/null || true - sudo sv start iofog-agent 2>/dev/null || true - ;; - upstart) - printf 'description "IoFog Agent container"\nstart on runlevel [2345]\nstop on runlevel [!2345]\nrespawn\nrespawn limit 10 5\nexec %s\n' "$RUN_CMD_FG" | sudo tee /etc/init/iofog-agent.conf > /dev/null - sudo initctl reload-configuration 2>/dev/null || true - sudo initctl start iofog-agent 2>/dev/null || true - ;; - *) - sudo tee /etc/init.d/iofog-agent > /dev/null < /dev/null -#!/bin/sh -CONTAINER_NAME="iofog-agent" -if ! podman ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "Error: The iofog-agent container is not running." - exit 1 -fi -exec podman exec ${CONTAINER_NAME} iofog-agent "$@" -EOF - else - cat <<'EOF' | sudo tee ${EXECUTABLE_FILE} > /dev/null -#!/bin/sh -CONTAINER_NAME="iofog-agent" -if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "Error: The iofog-agent container is not running." - exit 1 -fi -exec docker exec ${CONTAINER_NAME} iofog-agent "$@" -EOF - fi - sudo chmod +x ${EXECUTABLE_FILE} - - echo "ioFog agent installation completed!" -} - -do_start_iofog(){ - case "${INIT_SYSTEM:-systemd}" in - systemd) sudo systemctl start iofog-agent >/dev/null 2>&1 & ;; - sysvinit|openrc) sudo service iofog-agent start 2>/dev/null || sudo /etc/init.d/iofog-agent start 2>/dev/null & ;; - s6) sudo s6-svc -u /etc/s6/sv/iofog-agent 2>/dev/null & ;; - runit) sudo sv start iofog-agent 2>/dev/null & ;; - upstart) sudo initctl start iofog-agent 2>/dev/null & ;; - *) sudo systemctl start iofog-agent 2>/dev/null || sudo /etc/init.d/iofog-agent start 2>/dev/null & ;; - esac - local STATUS="" - local ITER=0 - while [ "$STATUS" != "RUNNING" ]; do - ITER=$((ITER+1)) - if [ "$ITER" -gt 600 ]; then - echo "Timed out waiting for Agent to be RUNNING" - exit 1 - fi - sleep 1 - STATUS=$(sudo iofog-agent status 2>/dev/null | cut -f2 -d: | head -n 1 | tr -d '[:space:]') - echo "${STATUS}" - done - sudo iofog-agent "config -cf 10 -sf 10" - if [ "$lsb_dist" = "rhel" ] || [ "$lsb_dist" = "centos" ] || [ "$lsb_dist" = "fedora" ] || [ "$lsb_dist" = "ol" ] || [ "$lsb_dist" = "sles" ] || [ "$lsb_dist" = "opensuse" ]; then - sudo iofog-agent "config -c unix:///var/run/podman/podman.sock" - fi -} - -agent_image="$1" -agent_tz="$2" -echo "Using variables" -echo "version: $agent_image" -echo "timezone: $agent_tz" -. /etc/iofog/agent/init.sh -init -do_check_install -do_stop_iofog -do_install_iofog -do_start_iofog - - - diff --git a/assets/airgap-agent/uninstall_iofog.sh b/assets/airgap-agent/uninstall_iofog.sh deleted file mode 100755 index 26c89c0e4..000000000 --- a/assets/airgap-agent/uninstall_iofog.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/bin/sh -set -x -set -e - -AGENT_CONFIG_FOLDER=iofog-agent-config -AGENT_LOG_FOLDER=/var/log/iofog-agent -AGENT_BACKUP_FOLDER=/var/backups/iofog-agent -AGENT_MESSAGE_FOLDER=/var/lib/iofog-agent -EXECUTABLE_FILE=/usr/local/bin/iofog-agent -CONTAINER_NAME="iofog-agent" - -do_uninstall_iofog() { - echo "# Removing ioFog agent..." - - case "$lsb_dist" in - rhel|fedora|centos|ol|sles|opensuse*) CONTAINER_RUNTIME="podman" ;; - *) CONTAINER_RUNTIME="docker" ;; - esac - - case "${INIT_SYSTEM:-systemd}" in - systemd) - for f in /etc/systemd/system/iofog-agent.service /etc/containers/systemd/iofog-agent.container; do - if [ -f "$f" ]; then - echo "Disabling and stopping systemd service..." - sudo systemctl stop iofog-agent.service 2>/dev/null || true - sudo systemctl disable iofog-agent.service 2>/dev/null || true - sudo rm -f "$f" - sudo systemctl daemon-reload - break - fi - done - ;; - sysvinit|openrc) - if [ -f /etc/init.d/iofog-agent ]; then - sudo service iofog-agent stop 2>/dev/null || sudo /etc/init.d/iofog-agent stop 2>/dev/null || true - [ "$INIT_SYSTEM" = "openrc" ] && sudo rc-update del iofog-agent default 2>/dev/null || true - sudo update-rc.d -f iofog-agent remove 2>/dev/null || sudo chkconfig --del iofog-agent 2>/dev/null || true - sudo rm -f /etc/init.d/iofog-agent - fi - ;; - s6) - sudo s6-svc -d /etc/s6/sv/iofog-agent 2>/dev/null || true - sudo rm -rf /etc/s6/sv/iofog-agent - [ -L /etc/s6/adminsv/default/iofog-agent ] && sudo rm -f /etc/s6/adminsv/default/iofog-agent - ;; - runit) - sudo sv stop iofog-agent 2>/dev/null || true - [ -L /var/service/iofog-agent ] && sudo rm -f /var/service/iofog-agent - [ -L /etc/runit/runsvdir/default/iofog-agent ] && sudo rm -f /etc/runit/runsvdir/default/iofog-agent - sudo rm -rf /etc/runit/sv/iofog-agent - ;; - upstart) - sudo initctl stop iofog-agent 2>/dev/null || true - sudo rm -f /etc/init/iofog-agent.conf - ;; - *) - sudo systemctl stop iofog-agent 2>/dev/null || true - sudo systemctl disable iofog-agent 2>/dev/null || true - sudo rm -f /etc/systemd/system/iofog-agent.service /etc/containers/systemd/iofog-agent.container - sudo systemctl daemon-reload 2>/dev/null || true - [ -f /etc/init.d/iofog-agent ] && sudo /etc/init.d/iofog-agent stop 2>/dev/null || true - sudo rm -f /etc/init.d/iofog-agent - ;; - esac - - if sudo ${CONTAINER_RUNTIME} ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then - echo "Stopping and removing the ioFog agent container..." - sudo ${CONTAINER_RUNTIME} stop ${CONTAINER_NAME} 2>/dev/null || true - sudo ${CONTAINER_RUNTIME} rm ${CONTAINER_NAME} 2>/dev/null || true - fi - - # Remove config files - echo "Checking if the ${CONTAINER_RUNTIME} volume exists..." - - if sudo ${CONTAINER_RUNTIME} volume inspect "${AGENT_CONFIG_FOLDER}" >/dev/null 2>&1; then - echo "${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' found. Removing..." - sudo ${CONTAINER_RUNTIME} volume rm "${AGENT_CONFIG_FOLDER}" - echo "${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' has been removed." - else - echo "${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' does not exist. Skipping removal." - fi - - # Remove log files - echo "Removing log files..." - sudo rm -rf ${AGENT_LOG_FOLDER} - - # Remove backup files - echo "Removing backup files..." - sudo rm -rf ${AGENT_BACKUP_FOLDER} - - # Remove message files - echo "Removing message files..." - sudo rm -rf ${AGENT_MESSAGE_FOLDER} - - # Remove the executable script - if [ -f ${EXECUTABLE_FILE} ]; then - echo "Removing the iofog-agent executable script..." - sudo rm -f ${EXECUTABLE_FILE} - fi - - echo "ioFog agent uninstalled successfully!" -} - -. /etc/iofog/agent/init.sh -init - -do_uninstall_iofog diff --git a/assets/airgap-controller/check_prereqs.sh b/assets/airgap-controller/check_prereqs.sh deleted file mode 100755 index a6b148d4a..000000000 --- a/assets/airgap-controller/check_prereqs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -x - -# Check can sudo without password -if ! $(sudo ls /tmp/ > /dev/null); then - MSG="Unable to successfully use sudo with user $USER on this host.\nUser $USER must be in sudoers group and using sudo without password must be enabled.\nPlease see iofog.org documentation for more details." - echo $MSG - exit 1 -fi \ No newline at end of file diff --git a/assets/airgap-controller/init.sh b/assets/airgap-controller/init.sh deleted file mode 100755 index 630898c81..000000000 --- a/assets/airgap-controller/init.sh +++ /dev/null @@ -1,296 +0,0 @@ -#!/bin/sh -# Script to detect Linux distribution and version -# Used as a precursor for system-specific installations - -# Exit on error and print commands for debugging -set -e -set -x - -# Define user variable -user="$(id -un 2>/dev/null || true)" - -# Check if a command exists -command_exists() { - command -v "$@" > /dev/null 2>&1 -} - -# Detect the Linux distribution -get_distribution() { - lsb_dist="" - dist_version="" - - # Every system that we officially support has /etc/os-release - if [ -r /etc/os-release ]; then - - lsb_dist="$(. /etc/os-release && echo "$ID")" - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')" - else - echo "Error: Unsupported Linux distribution! /etc/os-release not found." - exit 1 - fi - - echo "# Detected distribution: $lsb_dist (version: $dist_version)" -} - -# Check if this is a forked Linux distro -check_forked() { - # Skip if lsb_release doesn't exist - if ! command_exists lsb_release; then - return - fi - - # Check if the `-u` option is supported - set +e - lsb_release -a > /dev/null 2>&1 - lsb_release_exit_code=$? - set -e - - # Check if the command has exited successfully, it means we're in a forked distro - if [ "$lsb_release_exit_code" = "0" ]; then - # Get the upstream release info - current_lsb_dist=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]') - current_dist_version=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]') - - # Print info about current distro - echo "You're using '$current_lsb_dist' version '$current_dist_version'." - - # Check if current is different from detected (indicating a fork) - if [ "$current_lsb_dist" != "$lsb_dist" ] || [ "$current_dist_version" != "$dist_version" ]; then - echo "Upstream release is '$lsb_dist' version '$dist_version'." - fi - else - # Additional checks for specific distros that might not be properly detected - if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then - if [ "$lsb_dist" = "osmc" ]; then - # OSMC runs Raspbian - lsb_dist=raspbian - else - # We're Debian and don't even know it! - lsb_dist=debian - fi - # Get Debian version and map it to codename - dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" - case "$dist_version" in - 14) - dist_version="forky" - ;; - 13) - dist_version="trixie" - ;; - 12) - dist_version="bookworm" - ;; - 11) - dist_version="bullseye" - ;; - 10) - dist_version="buster" - ;; - 9) - dist_version="stretch" - ;; - 8|'Kali Linux 2') - dist_version="jessie" - ;; - 7) - dist_version="wheezy" - ;; - esac - elif [ -r /etc/redhat-release ] && [ -z "$lsb_dist" ]; then - lsb_dist=redhat - # Extract version from redhat-release file - dist_version="$(sed 's/.*release \([0-9.]*\).*/\1/' /etc/redhat-release)" - fi - fi -} - -# Set up sudo command if necessary -setup_sudo() { - sh_c='sh -c' - if [ "$user" != 'root' ]; then - if command_exists sudo; then - sh_c='sudo -E sh -c' - elif command_exists su; then - sh_c='su -c' - else - echo "Error: this installer needs the ability to run commands as root." - echo "We are unable to find either 'sudo' or 'su' available to make this happen." - exit 1 - fi - fi - echo "# Using command executor: $sh_c" -} - -# Refine distribution version detection based on the distro -refine_distribution_version() { - case "$lsb_dist" in - ubuntu) - if command_exists lsb_release; then - dist_version="$(lsb_release --codename | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then - - dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")" - fi - ;; - - debian|raspbian) - # If we only have a number, map it to a codename for better recognition - if echo "$dist_version" | grep -qE '^[0-9]+$'; then - case "$dist_version" in - 14) - dist_version="forky" - ;; - 13) - dist_version="trixie" - ;; - 12) - dist_version="bookworm" - ;; - 11) - dist_version="bullseye" - ;; - 10) - # Handle special case for Buster - dist_version="buster" - if [ "$user" = 'root' ]; then - apt-get update --allow-releaseinfo-change || true - elif command_exists sudo; then - sudo apt-get update --allow-releaseinfo-change || true - fi - ;; - 9) - dist_version="stretch" - ;; - 8) - dist_version="jessie" - ;; - 7) - dist_version="wheezy" - ;; - esac - fi - ;; - - centos|rhel|fedora|ol) - # Make sure we have a version number - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - if [ -z "$dist_version" ] && [ -r /etc/redhat-release ]; then - dist_version="$(sed 's/.*release \([0-9.]*\).*/\1/' /etc/redhat-release)" - fi - ;; - - sles|opensuse) - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - # Fallback for older versions - if [ -z "$dist_version" ] && [ -r /etc/SuSE-release ]; then - dist_version="$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = //')" - fi - # Ensure version is in the correct format (e.g., 15.4 for SLES 15 SP4) - if [ -n "$dist_version" ]; then - # Remove any non-numeric characters except dots - dist_version="$(echo "$dist_version" | sed 's/[^0-9.]//g')" - fi - # Normalize distribution name - if [ "$lsb_dist" = "sles" ]; then - lsb_dist="sles" - elif [ "$lsb_dist" = "opensuse" ]; then - lsb_dist="opensuse" - fi - ;; - - *) - if command_exists lsb_release; then - dist_version="$(lsb_release --release | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - ;; - esac -} - -# Detect init system -detect_init_system() { - if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then - INIT_SYSTEM="systemd" - elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then - INIT_SYSTEM="upstart" - elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then - INIT_SYSTEM="openrc" - elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then - INIT_SYSTEM="s6" - elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then - INIT_SYSTEM="runit" - elif [ -d /etc/init.d ]; then - INIT_SYSTEM="sysvinit" - else - INIT_SYSTEM="unknown" - fi - export INIT_SYSTEM - echo "# Detected init system: $INIT_SYSTEM" -} - -# Detect package type (deb, rpm, or other) -detect_package_type() { - case "$lsb_dist" in - debian|ubuntu|raspbian|mendel) - PACKAGE_TYPE="deb" - ;; - fedora|centos|rhel|ol|sles|opensuse*) - PACKAGE_TYPE="rpm" - ;; - alpine) - PACKAGE_TYPE="apk" - ;; - *) - if command_exists apt-get || command_exists dpkg; then - PACKAGE_TYPE="deb" - elif command_exists yum || command_exists dnf || command_exists zypper; then - PACKAGE_TYPE="rpm" - elif command_exists apk; then - PACKAGE_TYPE="apk" - else - PACKAGE_TYPE="other" - fi - ;; - esac - export PACKAGE_TYPE - echo "# Detected package type: $PACKAGE_TYPE" -} - -# Init function -init() { - # Detect basic distribution info - get_distribution - - # Set up sudo for privileged commands - setup_sudo - - # Refine version information - refine_distribution_version - - # Check if this is a forked distro - check_forked - - # Detect init system and package type (for universal OS/init support) - detect_init_system - detect_package_type - - # Print final distribution information - echo "----------------------------------------" - echo "Linux Distribution: $lsb_dist" - echo "Version: $dist_version" - echo "Init system: $INIT_SYSTEM" - echo "Package type: $PACKAGE_TYPE" - echo "----------------------------------------" - -} diff --git a/assets/airgap-controller/install_container_engine.sh b/assets/airgap-controller/install_container_engine.sh deleted file mode 100644 index 118917716..000000000 --- a/assets/airgap-controller/install_container_engine.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/bin/sh -# Script to configure Docker/Podman for airgap deployment -# Check-only: verifies container engine is installed (Docker 25+ or Podman 4+), then configures/starts -# Sources init.sh for distribution detection - -set -x -set -e - -CONTAINER_ENGINE_MSG="This operating system does not support automatic container engine installation. Please install Docker 25+ or Podman 4+ on the target host and re-run, or use an airgap deployment with a pre-installed engine." - -check_docker_version() { - docker_version_num=0 - if command -v docker >/dev/null 2>&1; then - raw=$(docker -v 2>/dev/null | sed 's/.*version \([^,]*\),.*/\1/' | tr -d '.') - [ -n "$raw" ] && docker_version_num="$raw" - fi - [ "$docker_version_num" -ge 2500 ] 2>/dev/null || return 1 -} - -check_podman_version() { - podman_version_num=0 - if command -v podman >/dev/null 2>&1; then - raw=$(podman --version 2>/dev/null | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p') - [ -n "$raw" ] && podman_version_num="$raw" - fi - [ "$podman_version_num" -ge 4 ] 2>/dev/null || return 1 -} - -start_docker() { - set +e - if $sh_c "docker ps" >/dev/null 2>&1; then - set -e - return 0 - fi - err_code=1 - case "${INIT_SYSTEM:-unknown}" in - systemd) $sh_c "systemctl start docker" >/dev/null 2>&1; err_code=$? ;; - sysvinit) $sh_c "service docker start" >/dev/null 2>&1 || $sh_c "/etc/init.d/docker start" >/dev/null 2>&1; err_code=$? ;; - openrc) $sh_c "rc-service docker start" >/dev/null 2>&1; err_code=$? ;; - *) - $sh_c "/etc/init.d/docker start" >/dev/null 2>&1 - err_code=$? - [ $err_code -ne 0 ] && $sh_c "systemctl start docker" >/dev/null 2>&1 && err_code=0 - [ $err_code -ne 0 ] && $sh_c "service docker start" >/dev/null 2>&1 && err_code=0 - [ $err_code -ne 0 ] && $sh_c "snap start docker" >/dev/null 2>&1 && err_code=0 - ;; - esac - set -e - if [ $err_code -ne 0 ]; then - echo "Could not start Docker daemon" - exit 1 - fi -} - -start_podman() { - set +e - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl start podman" >/dev/null 2>&1 - $sh_c "systemctl start podman.socket" >/dev/null 2>&1 - ;; - sysvinit) $sh_c "service podman start" >/dev/null 2>&1 || $sh_c "/etc/init.d/podman start" >/dev/null 2>&1 ;; - openrc) $sh_c "rc-service podman start" >/dev/null 2>&1 ;; - *) - $sh_c "systemctl start podman" >/dev/null 2>&1 || true - $sh_c "systemctl start podman.socket" >/dev/null 2>&1 || true - $sh_c "service podman start" >/dev/null 2>&1 || true - ;; - esac - set -e -} - -do_modify_daemon() { - # Skip for Podman installations - if [ "$USE_PODMAN" = "true" ]; then - echo "# Configuring Podman for CDI directory support..." - - # Create CDI directories - $sh_c "mkdir -p /etc/cdi /var/run/cdi" - - # Ensure /etc/containers exists - $sh_c "mkdir -p /etc/containers" - - # Create containers.conf if it doesn't exist - if [ ! -f "/etc/containers/containers.conf" ]; then - $sh_c 'cat > /etc/containers/containers.conf <> /etc/containers/containers.conf' - fi - fi - - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl enable podman" 2>/dev/null || true - $sh_c "systemctl enable podman.socket" 2>/dev/null || true - ;; - openrc) $sh_c "rc-update add podman default" 2>/dev/null || true ;; - sysvinit) $sh_c "update-rc.d podman defaults" 2>/dev/null || $sh_c "chkconfig podman on" 2>/dev/null || true ;; - *) ;; - esac - start_podman - return - fi - - # Original Docker daemon configuration - if [ ! -f /etc/docker/daemon.json ]; then - echo "Creating /etc/docker/daemon.json..." - $sh_c "mkdir -p /etc/docker" - $sh_c 'cat > /etc/docker/daemon.json << EOF -{ - "storage-driver": "overlayfs", - "features": { - "containerd-snapshotter": true, - "cdi": true - }, - "cdi-spec-dirs": ["/etc/cdi/", "/var/run/cdi"] -} -EOF' - else - echo "/etc/docker/daemon.json already exists" - fi - echo "Restarting Docker daemon..." - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl daemon-reload" - $sh_c "systemctl restart docker" - ;; - *) - $sh_c "systemctl daemon-reload" 2>/dev/null || true - $sh_c "systemctl restart docker" 2>/dev/null || start_docker - ;; - esac -} - -# Airgap: determine engine by availability (Docker 25+ or Podman 4+) -determine_container_engine() { - if check_docker_version; then - USE_PODMAN="false" - echo "# Using Docker (25+)" - elif check_podman_version; then - USE_PODMAN="true" - echo "# Using Podman (4+)" - else - echo "Error: Docker 25+ or Podman 4+ is required. $CONTAINER_ENGINE_MSG" - exit 1 - fi -} - -. /etc/iofog/controller/init.sh -init - -determine_container_engine - -if [ "$USE_PODMAN" = "false" ]; then - start_docker -fi - -do_modify_daemon - -echo "# Container engine configuration completed successfully" - - - diff --git a/assets/airgap-controller/install_iofog.sh b/assets/airgap-controller/install_iofog.sh deleted file mode 100644 index 1ec2e73dc..000000000 --- a/assets/airgap-controller/install_iofog.sh +++ /dev/null @@ -1,222 +0,0 @@ -#!/bin/sh -set -x -set -e - -# INSTALL_DIR="/opt/iofog" -TMP_DIR="/tmp/iofog" -ETC_DIR="/etc/iofog/controller" -CONTROLLER_LOG_FOLDER=/var/log/iofog-controller -CONTROLLER_CONTAINER_NAME="iofog-controller" - -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -do_stop_iofog_controller() { - if ! command_exists iofog-controller; then - return 0 - fi - case "${INIT_SYSTEM:-systemd}" in - systemd) sudo systemctl stop iofog-controller 2>/dev/null || true ;; - sysvinit|openrc) sudo service iofog-controller stop 2>/dev/null || sudo /etc/init.d/iofog-controller stop 2>/dev/null || true ;; - s6) sudo s6-svc -d /etc/s6/sv/iofog-controller 2>/dev/null || true ;; - runit) sudo sv stop iofog-controller 2>/dev/null || true ;; - upstart) sudo initctl stop iofog-controller 2>/dev/null || true ;; - *) sudo systemctl stop iofog-controller 2>/dev/null || sudo service iofog-controller stop 2>/dev/null || true ;; - esac - (docker stop ${CONTROLLER_CONTAINER_NAME} 2>/dev/null || podman stop ${CONTROLLER_CONTAINER_NAME} 2>/dev/null) || true -} - -do_install_iofog_controller() { - echo "# Installing ioFog controller (airgap mode)..." - - for FOLDER in ${ETC_DIR} ${CONTROLLER_LOG_FOLDER}; do - if [ ! -d "$FOLDER" ]; then - echo "Creating folder: $FOLDER" - sudo mkdir -p "$FOLDER" - sudo chmod 775 "$FOLDER" - fi - done - - USE_PODMAN="false" - case "$lsb_dist" in - rhel|centos|fedora|ol|sles|opensuse*) USE_PODMAN="true" ;; - esac - - CONTROLLER_RUN_ARGS="-e IOFOG_CONTROLLER_IMAGE=${controller_image} --env-file ${ETC_DIR}/iofog-controller.env -v iofog-controller-db:/home/runner/.npm-global/lib/node_modules/@eclipse-iofog/iofogcontroller/src/data/sqlite_files/:rw -v iofog-controller-log:/var/log/iofog-controller:rw -p 51121:51121 -p 80:8008 --stop-timeout 60 ${controller_image}" - - if [ "${INIT_SYSTEM:-systemd}" = "systemd" ]; then - if [ "$USE_PODMAN" = "true" ]; then - echo "Creating Quadlet container file for ioFog controller..." - sudo mkdir -p /etc/containers/systemd - cat < /dev/null -[Unit] -Description=ioFog Controller Service -After=podman.service -Requires=podman.service - -[Container] -ContainerName=${CONTROLLER_CONTAINER_NAME} -Image=${controller_image} -PodmanArgs=--stop-timeout=60 -Environment=IOFOG_CONTROLLER_IMAGE=${controller_image} -EnvironmentFile=${ETC_DIR}/iofog-controller.env -Volume=iofog-controller-db:/home/runner/.npm-global/lib/node_modules/@eclipse-iofog/iofogcontroller/src/data/sqlite_files/:rw -Volume=iofog-controller-log:/var/log/iofog-controller:rw -PublishPort=51121:51121 -PublishPort=80:8008 -LogDriver=journald - -[Service] -Restart=always - -[Install] -WantedBy=default.target -EOF - sudo systemctl daemon-reload - sudo systemctl restart podman 2>/dev/null || true - sudo systemctl enable iofog-controller.service - sudo systemctl start iofog-controller.service - else - echo "Creating systemd service for ioFog controller..." - cat < /dev/null -[Unit] -Description=ioFog Controller Service -After=docker.service -Requires=docker.service - -[Service] -TimeoutStartSec=0 -Restart=always -ExecStartPre=-/usr/bin/docker rm -f ${CONTROLLER_CONTAINER_NAME} -ExecStart=/usr/bin/docker run --rm --name ${CONTROLLER_CONTAINER_NAME} \\ -${CONTROLLER_RUN_ARGS} -ExecStop=/usr/bin/docker stop ${CONTROLLER_CONTAINER_NAME} - -[Install] -WantedBy=default.target -EOF - sudo systemctl daemon-reload - sudo systemctl enable iofog-controller.service - sudo systemctl start iofog-controller.service - fi - else - if [ "$USE_PODMAN" = "true" ]; then - RUN_CMD="podman run --rm -d --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}" - RUN_CMD_FG="podman run --rm --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}" - else - RUN_CMD="docker run --rm -d --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}" - RUN_CMD_FG="docker run --rm --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}" - fi - if [ "$USE_PODMAN" = "true" ]; then - STOP_CMD="podman stop ${CONTROLLER_CONTAINER_NAME}" - else - STOP_CMD="docker stop ${CONTROLLER_CONTAINER_NAME}" - fi - case "$INIT_SYSTEM" in - sysvinit|openrc) - sudo tee /etc/init.d/iofog-controller > /dev/null </dev/null | grep -q "^${CONTROLLER_CONTAINER_NAME}\$"; then exit 0; fi - if podman ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTROLLER_CONTAINER_NAME}\$"; then exit 0; fi - $RUN_CMD - ;; - stop) $STOP_CMD 2>/dev/null || true ;; - restart) \$0 stop; \$0 start ;; - status) - if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTROLLER_CONTAINER_NAME}\$"; then echo "running"; exit 0; fi - if podman ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTROLLER_CONTAINER_NAME}\$"; then echo "running"; exit 0; fi - echo "stopped"; exit 1 - ;; - *) echo "Usage: \$0 {start|stop|restart|status}"; exit 1 ;; -esac -exit 0 -INITSCRIPT - sudo chmod +x /etc/init.d/iofog-controller - if [ "$INIT_SYSTEM" = "openrc" ]; then - sudo rc-update add iofog-controller default 2>/dev/null || true - sudo rc-service iofog-controller start - else - sudo update-rc.d iofog-controller defaults 2>/dev/null || sudo chkconfig iofog-controller on 2>/dev/null || true - sudo service iofog-controller start 2>/dev/null || sudo /etc/init.d/iofog-controller start - fi - ;; - s6) - sudo mkdir -p /etc/s6/sv/iofog-controller - printf '#!/bin/sh\nexec %s\n' "$RUN_CMD_FG" | sudo tee /etc/s6/sv/iofog-controller/run > /dev/null - sudo chmod +x /etc/s6/sv/iofog-controller/run - [ -d /etc/s6/adminsv/default ] && sudo ln -sf /etc/s6/sv/iofog-controller /etc/s6/adminsv/default/iofog-controller 2>/dev/null || true - sudo s6-svc -u /etc/s6/sv/iofog-controller 2>/dev/null || true - ;; - runit) - sudo mkdir -p /etc/runit/sv/iofog-controller - printf '#!/bin/sh\nexec %s\n' "$RUN_CMD_FG" | sudo tee /etc/runit/sv/iofog-controller/run > /dev/null - sudo chmod +x /etc/runit/sv/iofog-controller/run - [ -d /var/service ] && sudo ln -sf /etc/runit/sv/iofog-controller /var/service/iofog-controller 2>/dev/null || true - [ -d /etc/runit/runsvdir/default ] && sudo ln -sf /etc/runit/sv/iofog-controller /etc/runit/runsvdir/default/iofog-controller 2>/dev/null || true - sudo sv start iofog-controller 2>/dev/null || true - ;; - upstart) - printf 'description "IoFog Controller container"\nstart on runlevel [2345]\nstop on runlevel [!2345]\nrespawn\nrespawn limit 10 5\nexec %s\n' "$RUN_CMD_FG" | sudo tee /etc/init/iofog-controller.conf > /dev/null - sudo initctl reload-configuration 2>/dev/null || true - sudo initctl start iofog-controller 2>/dev/null || true - ;; - *) - sudo tee /etc/init.d/iofog-controller > /dev/null < /dev/null -#!/bin/sh -CONTAINER_NAME="iofog-controller" -if ! podman ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "Error: The iofog-controller container is not running." - exit 1 -fi -exec podman exec ${CONTAINER_NAME} iofog-controller "$@" -EOF - else - cat <<'EOF' | sudo tee ${EXECUTABLE_FILE} > /dev/null -#!/bin/sh -CONTAINER_NAME="iofog-controller" -if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "Error: The iofog-controller container is not running." - exit 1 -fi -exec docker exec ${CONTAINER_NAME} iofog-controller "$@" -EOF - fi - sudo chmod +x ${EXECUTABLE_FILE} - - echo "ioFog controller installation completed!" -} - -# main -controller_image="$1" - -. /etc/iofog/controller/init.sh -init -do_stop_iofog_controller -do_install_iofog_controller - - - diff --git a/assets/airgap-controller/set_env.sh b/assets/airgap-controller/set_env.sh deleted file mode 100755 index 877e4a716..000000000 --- a/assets/airgap-controller/set_env.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -set -x -set -e - -ETC_DIR="/etc/iofog/controller" -ENV_FILE_NAME=iofog-controller.env # Used as an env file in systemd - -ENV_FILE="$ETC_DIR/$ENV_FILE_NAME" - -# Create folder -mkdir -p "$ETC_DIR" - -# Env file (for systemd) -rm -f "$ENV_FILE" -touch "$ENV_FILE" - -for var in "$@" -do - echo "$var" >> "$ENV_FILE" -done \ No newline at end of file diff --git a/assets/airgap-controller/uninstall_iofog.sh b/assets/airgap-controller/uninstall_iofog.sh deleted file mode 100644 index 3356c92f0..000000000 --- a/assets/airgap-controller/uninstall_iofog.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/sh -set -x -set -e - - -CONTROLLER_LOG_DIR="iofog-controller-log" -CONTAINER_NAME="iofog-controller" -EXECUTABLE_FILE=/usr/local/bin/iofog-controller -CONTROLLER_DB=iofog-controller-db - - -do_uninstall_controller() { - echo "# Removing ioFog controller..." - - case "$lsb_dist" in - rhel|fedora|centos|ol|sles|opensuse*) CONTAINER_RUNTIME="podman" ;; - *) CONTAINER_RUNTIME="docker" ;; - esac - - case "${INIT_SYSTEM:-systemd}" in - systemd) - for f in /etc/systemd/system/iofog-controller.service /etc/containers/systemd/iofog-controller.container; do - if [ -f "$f" ]; then - echo "Disabling and stopping systemd service..." - sudo systemctl stop iofog-controller.service 2>/dev/null || true - sudo systemctl disable iofog-controller.service 2>/dev/null || true - sudo rm -f "$f" - sudo systemctl daemon-reload - break - fi - done - ;; - sysvinit|openrc) - if [ -f /etc/init.d/iofog-controller ]; then - sudo service iofog-controller stop 2>/dev/null || sudo /etc/init.d/iofog-controller stop 2>/dev/null || true - [ "$INIT_SYSTEM" = "openrc" ] && sudo rc-update del iofog-controller default 2>/dev/null || true - sudo update-rc.d -f iofog-controller remove 2>/dev/null || sudo chkconfig --del iofog-controller 2>/dev/null || true - sudo rm -f /etc/init.d/iofog-controller - fi - ;; - s6) - sudo s6-svc -d /etc/s6/sv/iofog-controller 2>/dev/null || true - sudo rm -rf /etc/s6/sv/iofog-controller - [ -L /etc/s6/adminsv/default/iofog-controller ] && sudo rm -f /etc/s6/adminsv/default/iofog-controller - ;; - runit) - sudo sv stop iofog-controller 2>/dev/null || true - [ -L /var/service/iofog-controller ] && sudo rm -f /var/service/iofog-controller - [ -L /etc/runit/runsvdir/default/iofog-controller ] && sudo rm -f /etc/runit/runsvdir/default/iofog-controller - sudo rm -rf /etc/runit/sv/iofog-controller - ;; - upstart) - sudo initctl stop iofog-controller 2>/dev/null || true - sudo rm -f /etc/init/iofog-controller.conf - ;; - *) - sudo systemctl stop iofog-controller 2>/dev/null || true - sudo systemctl disable iofog-controller 2>/dev/null || true - sudo rm -f /etc/systemd/system/iofog-controller.service /etc/containers/systemd/iofog-controller.container - sudo systemctl daemon-reload 2>/dev/null || true - [ -f /etc/init.d/iofog-controller ] && sudo /etc/init.d/iofog-controller stop 2>/dev/null || true - sudo rm -f /etc/init.d/iofog-controller - ;; - esac - - if sudo ${CONTAINER_RUNTIME} ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then - echo "Stopping and removing the ioFog controller container..." - sudo ${CONTAINER_RUNTIME} stop ${CONTAINER_NAME} 2>/dev/null || true - sudo ${CONTAINER_RUNTIME} rm ${CONTAINER_NAME} 2>/dev/null || true - fi - - # Remove config files - echo "Checking if the ${CONTAINER_RUNTIME} volume exists..." - - if sudo ${CONTAINER_RUNTIME} volume inspect "${CONTROLLER_DB}" >/dev/null 2>&1; then - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' found. Removing..." - sudo ${CONTAINER_RUNTIME} volume rm "${CONTROLLER_DB}" - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' has been removed." - else - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' does not exist. Skipping removal." - fi - - # Remove log files - echo "Removing log files..." - if sudo ${CONTAINER_RUNTIME} volume inspect "${CONTROLLER_LOG_DIR}" >/dev/null 2>&1; then - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' found. Removing..." - sudo ${CONTAINER_RUNTIME} volume rm "${CONTROLLER_LOG_DIR}" - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' has been removed." - else - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' does not exist. Skipping removal." - fi - - - # Remove the executable script - if [ -f ${EXECUTABLE_FILE} ]; then - echo "Removing the iofog-controller executable script..." - sudo rm -f ${EXECUTABLE_FILE} - fi - - echo "ioFog controller uninstalled successfully!" -} - -. /etc/iofog/controller/init.sh -init - -do_uninstall_controller \ No newline at end of file diff --git a/assets/container-agent/check_prereqs.sh b/assets/container-agent/check_prereqs.sh deleted file mode 100755 index a6b148d4a..000000000 --- a/assets/container-agent/check_prereqs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -x - -# Check can sudo without password -if ! $(sudo ls /tmp/ > /dev/null); then - MSG="Unable to successfully use sudo with user $USER on this host.\nUser $USER must be in sudoers group and using sudo without password must be enabled.\nPlease see iofog.org documentation for more details." - echo $MSG - exit 1 -fi \ No newline at end of file diff --git a/assets/container-agent/init.sh b/assets/container-agent/init.sh deleted file mode 100755 index 630898c81..000000000 --- a/assets/container-agent/init.sh +++ /dev/null @@ -1,296 +0,0 @@ -#!/bin/sh -# Script to detect Linux distribution and version -# Used as a precursor for system-specific installations - -# Exit on error and print commands for debugging -set -e -set -x - -# Define user variable -user="$(id -un 2>/dev/null || true)" - -# Check if a command exists -command_exists() { - command -v "$@" > /dev/null 2>&1 -} - -# Detect the Linux distribution -get_distribution() { - lsb_dist="" - dist_version="" - - # Every system that we officially support has /etc/os-release - if [ -r /etc/os-release ]; then - - lsb_dist="$(. /etc/os-release && echo "$ID")" - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')" - else - echo "Error: Unsupported Linux distribution! /etc/os-release not found." - exit 1 - fi - - echo "# Detected distribution: $lsb_dist (version: $dist_version)" -} - -# Check if this is a forked Linux distro -check_forked() { - # Skip if lsb_release doesn't exist - if ! command_exists lsb_release; then - return - fi - - # Check if the `-u` option is supported - set +e - lsb_release -a > /dev/null 2>&1 - lsb_release_exit_code=$? - set -e - - # Check if the command has exited successfully, it means we're in a forked distro - if [ "$lsb_release_exit_code" = "0" ]; then - # Get the upstream release info - current_lsb_dist=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]') - current_dist_version=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]') - - # Print info about current distro - echo "You're using '$current_lsb_dist' version '$current_dist_version'." - - # Check if current is different from detected (indicating a fork) - if [ "$current_lsb_dist" != "$lsb_dist" ] || [ "$current_dist_version" != "$dist_version" ]; then - echo "Upstream release is '$lsb_dist' version '$dist_version'." - fi - else - # Additional checks for specific distros that might not be properly detected - if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then - if [ "$lsb_dist" = "osmc" ]; then - # OSMC runs Raspbian - lsb_dist=raspbian - else - # We're Debian and don't even know it! - lsb_dist=debian - fi - # Get Debian version and map it to codename - dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" - case "$dist_version" in - 14) - dist_version="forky" - ;; - 13) - dist_version="trixie" - ;; - 12) - dist_version="bookworm" - ;; - 11) - dist_version="bullseye" - ;; - 10) - dist_version="buster" - ;; - 9) - dist_version="stretch" - ;; - 8|'Kali Linux 2') - dist_version="jessie" - ;; - 7) - dist_version="wheezy" - ;; - esac - elif [ -r /etc/redhat-release ] && [ -z "$lsb_dist" ]; then - lsb_dist=redhat - # Extract version from redhat-release file - dist_version="$(sed 's/.*release \([0-9.]*\).*/\1/' /etc/redhat-release)" - fi - fi -} - -# Set up sudo command if necessary -setup_sudo() { - sh_c='sh -c' - if [ "$user" != 'root' ]; then - if command_exists sudo; then - sh_c='sudo -E sh -c' - elif command_exists su; then - sh_c='su -c' - else - echo "Error: this installer needs the ability to run commands as root." - echo "We are unable to find either 'sudo' or 'su' available to make this happen." - exit 1 - fi - fi - echo "# Using command executor: $sh_c" -} - -# Refine distribution version detection based on the distro -refine_distribution_version() { - case "$lsb_dist" in - ubuntu) - if command_exists lsb_release; then - dist_version="$(lsb_release --codename | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then - - dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")" - fi - ;; - - debian|raspbian) - # If we only have a number, map it to a codename for better recognition - if echo "$dist_version" | grep -qE '^[0-9]+$'; then - case "$dist_version" in - 14) - dist_version="forky" - ;; - 13) - dist_version="trixie" - ;; - 12) - dist_version="bookworm" - ;; - 11) - dist_version="bullseye" - ;; - 10) - # Handle special case for Buster - dist_version="buster" - if [ "$user" = 'root' ]; then - apt-get update --allow-releaseinfo-change || true - elif command_exists sudo; then - sudo apt-get update --allow-releaseinfo-change || true - fi - ;; - 9) - dist_version="stretch" - ;; - 8) - dist_version="jessie" - ;; - 7) - dist_version="wheezy" - ;; - esac - fi - ;; - - centos|rhel|fedora|ol) - # Make sure we have a version number - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - if [ -z "$dist_version" ] && [ -r /etc/redhat-release ]; then - dist_version="$(sed 's/.*release \([0-9.]*\).*/\1/' /etc/redhat-release)" - fi - ;; - - sles|opensuse) - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - # Fallback for older versions - if [ -z "$dist_version" ] && [ -r /etc/SuSE-release ]; then - dist_version="$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = //')" - fi - # Ensure version is in the correct format (e.g., 15.4 for SLES 15 SP4) - if [ -n "$dist_version" ]; then - # Remove any non-numeric characters except dots - dist_version="$(echo "$dist_version" | sed 's/[^0-9.]//g')" - fi - # Normalize distribution name - if [ "$lsb_dist" = "sles" ]; then - lsb_dist="sles" - elif [ "$lsb_dist" = "opensuse" ]; then - lsb_dist="opensuse" - fi - ;; - - *) - if command_exists lsb_release; then - dist_version="$(lsb_release --release | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - ;; - esac -} - -# Detect init system -detect_init_system() { - if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then - INIT_SYSTEM="systemd" - elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then - INIT_SYSTEM="upstart" - elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then - INIT_SYSTEM="openrc" - elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then - INIT_SYSTEM="s6" - elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then - INIT_SYSTEM="runit" - elif [ -d /etc/init.d ]; then - INIT_SYSTEM="sysvinit" - else - INIT_SYSTEM="unknown" - fi - export INIT_SYSTEM - echo "# Detected init system: $INIT_SYSTEM" -} - -# Detect package type (deb, rpm, or other) -detect_package_type() { - case "$lsb_dist" in - debian|ubuntu|raspbian|mendel) - PACKAGE_TYPE="deb" - ;; - fedora|centos|rhel|ol|sles|opensuse*) - PACKAGE_TYPE="rpm" - ;; - alpine) - PACKAGE_TYPE="apk" - ;; - *) - if command_exists apt-get || command_exists dpkg; then - PACKAGE_TYPE="deb" - elif command_exists yum || command_exists dnf || command_exists zypper; then - PACKAGE_TYPE="rpm" - elif command_exists apk; then - PACKAGE_TYPE="apk" - else - PACKAGE_TYPE="other" - fi - ;; - esac - export PACKAGE_TYPE - echo "# Detected package type: $PACKAGE_TYPE" -} - -# Init function -init() { - # Detect basic distribution info - get_distribution - - # Set up sudo for privileged commands - setup_sudo - - # Refine version information - refine_distribution_version - - # Check if this is a forked distro - check_forked - - # Detect init system and package type (for universal OS/init support) - detect_init_system - detect_package_type - - # Print final distribution information - echo "----------------------------------------" - echo "Linux Distribution: $lsb_dist" - echo "Version: $dist_version" - echo "Init system: $INIT_SYSTEM" - echo "Package type: $PACKAGE_TYPE" - echo "----------------------------------------" - -} diff --git a/assets/container-agent/install_container_engine.sh b/assets/container-agent/install_container_engine.sh deleted file mode 100755 index 8c4334587..000000000 --- a/assets/container-agent/install_container_engine.sh +++ /dev/null @@ -1,302 +0,0 @@ -#!/bin/sh -# Script to install Docker/Podman based on Linux distribution -# Sources init.sh for distribution detection - -set -x -set -e - -CONTAINER_ENGINE_MSG="This operating system does not support automatic container engine installation. Please install Docker 25+ or Podman 4+ on the target host and re-run, or use an airgap deployment with a pre-installed engine." - -check_docker_version() { - docker_version_num=0 - if command -v docker >/dev/null 2>&1; then - raw=$(docker -v 2>/dev/null | sed 's/.*version \([^,]*\),.*/\1/' | tr -d '.') - [ -n "$raw" ] && docker_version_num="$raw" - fi - [ "$docker_version_num" -ge 2500 ] 2>/dev/null || return 1 -} - -check_podman_version() { - podman_version_num=0 - if command -v podman >/dev/null 2>&1; then - raw=$(podman --version 2>/dev/null | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p') - [ -n "$raw" ] && podman_version_num="$raw" - fi - [ "$podman_version_num" -ge 4 ] 2>/dev/null || return 1 -} - -start_docker() { - set +e - if $sh_c "docker ps" >/dev/null 2>&1; then - set -e - return 0 - fi - err_code=1 - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl start docker" >/dev/null 2>&1 - err_code=$? - ;; - sysvinit) - $sh_c "service docker start" >/dev/null 2>&1 || $sh_c "/etc/init.d/docker start" >/dev/null 2>&1 - err_code=$? - ;; - openrc) - $sh_c "rc-service docker start" >/dev/null 2>&1 - err_code=$? - ;; - *) - $sh_c "/etc/init.d/docker start" >/dev/null 2>&1 - err_code=$? - [ $err_code -ne 0 ] && $sh_c "systemctl start docker" >/dev/null 2>&1 && err_code=0 - [ $err_code -ne 0 ] && $sh_c "service docker start" >/dev/null 2>&1 && err_code=0 - [ $err_code -ne 0 ] && $sh_c "snap start docker" >/dev/null 2>&1 && err_code=0 - ;; - esac - set -e - if [ $err_code -ne 0 ]; then - echo "Could not start Docker daemon" - exit 1 - fi -} - -start_podman() { - set +e - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl start podman" >/dev/null 2>&1 - $sh_c "systemctl start podman.socket" >/dev/null 2>&1 - ;; - sysvinit) - $sh_c "service podman start" >/dev/null 2>&1 || $sh_c "/etc/init.d/podman start" >/dev/null 2>&1 - ;; - openrc) - $sh_c "rc-service podman start" >/dev/null 2>&1 - ;; - *) - $sh_c "systemctl start podman" >/dev/null 2>&1 || true - $sh_c "systemctl start podman.socket" >/dev/null 2>&1 || true - $sh_c "service podman start" >/dev/null 2>&1 || true - ;; - esac - set -e -} - - -do_modify_daemon() { - # Skip for Podman installations - if [ "$USE_PODMAN" = "true" ]; then - echo "# Configuring Podman for CDI directory support..." - - # Create CDI directories - $sh_c "mkdir -p /etc/cdi /var/run/cdi" - - # Ensure /etc/containers exists - $sh_c "mkdir -p /etc/containers" - - # Create containers.conf if it doesn't exist - if [ ! -f "/etc/containers/containers.conf" ]; then - $sh_c 'cat > /etc/containers/containers.conf <> /etc/containers/containers.conf' - fi - fi - - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl enable podman" 2>/dev/null || true - $sh_c "systemctl enable podman.socket" 2>/dev/null || true - ;; - openrc) - $sh_c "rc-update add podman default" 2>/dev/null || true - ;; - sysvinit) - $sh_c "update-rc.d podman defaults" 2>/dev/null || $sh_c "chkconfig podman on" 2>/dev/null || true - ;; - *) ;; - esac - start_podman - return - fi - - # Original Docker daemon configuration - if [ ! -f /etc/docker/daemon.json ]; then - echo "Creating /etc/docker/daemon.json..." - $sh_c "mkdir -p /etc/docker" - $sh_c 'cat > /etc/docker/daemon.json << EOF -{ - "storage-driver": "overlayfs", - "features": { - "containerd-snapshotter": true, - "cdi": true - }, - "cdi-spec-dirs": ["/etc/cdi/", "/var/run/cdi"] -} -EOF' - else - echo "/etc/docker/daemon.json already exists" - fi - echo "Restarting Docker daemon..." - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl daemon-reload" - $sh_c "systemctl restart docker" - ;; - *) - $sh_c "systemctl daemon-reload" 2>/dev/null || true - $sh_c "systemctl restart docker" 2>/dev/null || start_docker - ;; - esac -} - -do_install_container_engine() { - if [ "$PACKAGE_TYPE" = "apk" ]; then - if command_exists docker && check_docker_version; then - echo "# Docker already installed (>= 25)" - start_docker - do_modify_daemon - return 0 - fi - echo "# Installing Docker on Alpine..." - $sh_c "apk add docker" - $sh_c "rc-update add docker default" - $sh_c "service docker start" - $sh_c "addgroup $user docker" - if ! command_exists docker; then - echo "Failed to install Docker" - exit 1 - fi - if ! check_docker_version; then - echo "Error: Docker 25+ is required. Please upgrade the Docker package or install Docker 25+ manually." - exit 1 - fi - start_docker - do_modify_daemon - return 0 - fi - - if [ "$PACKAGE_TYPE" = "other" ]; then - if check_docker_version; then - USE_PODMAN="false" - echo "# Docker (>= 25) found; using Docker." - start_docker - do_modify_daemon - return 0 - fi - if check_podman_version; then - USE_PODMAN="true" - echo "# Podman (>= 4) found; using Podman." - do_modify_daemon - return 0 - fi - echo "Error: $CONTAINER_ENGINE_MSG" - exit 1 - fi - - if [ "$USE_PODMAN" = "true" ]; then - echo "# Installing Podman and related packages..." - case "$lsb_dist" in - fedora|centos|rhel|ol) - $sh_c "yum install -y podman crun podman-docker" - ;; - sles|opensuse*) - $sh_c "zypper install -y podman crun podman-docker" - ;; - esac - if ! check_podman_version; then - echo "Error: Podman 4+ is required. Please upgrade Podman." - exit 1 - fi - do_modify_daemon - return - fi - - if command_exists docker; then - docker_version=$(docker -v 2>/dev/null | sed 's/.*version \([^,]*\),.*/\1/' | tr -d '.') - if [ -n "$docker_version" ] && [ "$docker_version" -ge 2500 ] 2>/dev/null; then - echo "# Docker already installed (>= 25)" - start_docker - do_modify_daemon - return - fi - fi - - echo "# Installing Docker..." - case "$lsb_dist" in - debian|ubuntu|raspbian) - case "$dist_version" in - "stretch") - $sh_c "apt install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common" - curl -fsSL https://download.docker.com/linux/debian/gpg | $sh_c "apt-key add -" - $sh_c "add-apt-repository \"deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/debian $(lsb_release -cs) stable\"" - $sh_c "apt update -y" - $sh_c "apt install -y docker-ce" - ;; - *) - curl -fsSL https://get.docker.com/ | $sh_c "sh" - ;; - esac - ;; - *) - curl -fsSL https://get.docker.com/ | $sh_c "sh" - ;; - esac - - if ! command_exists docker; then - echo "Failed to install Docker" - exit 1 - fi - if ! check_docker_version; then - echo "Error: Docker 25+ is required. Please upgrade Docker." - exit 1 - fi - start_docker - do_modify_daemon -} - -# Check if we should use Podman based on distribution -determine_container_engine() { - USE_PODMAN="false" - case "$lsb_dist" in - fedora|centos|rhel|ol|sles|opensuse*) - USE_PODMAN="true" - echo "# Using Podman for $lsb_dist" - ;; - *) - echo "# Using Docker for $lsb_dist" - ;; - esac -} - -# Source init.sh to get distribution info -. /etc/iofog/agent/init.sh -init - -# Configure container engine based on distribution -determine_container_engine - -# Install appropriate container engine -do_install_container_engine - -echo "# Installation completed successfully" \ No newline at end of file diff --git a/assets/container-agent/install_deps.sh b/assets/container-agent/install_deps.sh deleted file mode 100755 index ab04c7f99..000000000 --- a/assets/container-agent/install_deps.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -x -set -e - - -/etc/iofog/agent/install_container_engine.sh diff --git a/assets/container-agent/install_iofog.sh b/assets/container-agent/install_iofog.sh deleted file mode 100755 index 5be7b9208..000000000 --- a/assets/container-agent/install_iofog.sh +++ /dev/null @@ -1,339 +0,0 @@ -#!/bin/sh -set -x -set -e - -AGENT_LOG_FOLDER=/var/log/iofog-agent -AGENT_BACKUP_FOLDER=/var/backups/iofog-agent -AGENT_MESSAGE_FOLDER=/var/lib/iofog-agent -AGENT_SHARE_FOLDER=/usr/share/iofog-agent -SAVED_AGENT_CONFIG_FOLDER=/tmp/agent-config-save -AGENT_CONTAINER_NAME="iofog-agent" -ETC_DIR=/etc/iofog/agent - -do_check_install() { - if command_exists iofog-agent; then - local VERSION=$(sudo iofog-agent version | head -n1 | sed "s/ioFog//g" | tr -d ' ' | tr -d "\n") - if [ "$VERSION" = "$agent_version" ]; then - echo "Agent $VERSION already installed." - exit 0 - fi - fi -} - -do_stop_iofog() { - if ! command_exists iofog-agent; then - return 0 - fi - case "${INIT_SYSTEM:-systemd}" in - systemd) - sudo systemctl stop iofog-agent 2>/dev/null || true - ;; - sysvinit|openrc) - sudo service iofog-agent stop 2>/dev/null || sudo /etc/init.d/iofog-agent stop 2>/dev/null || true - ;; - s6) - sudo s6-svc -d /etc/s6/sv/iofog-agent 2>/dev/null || true - ;; - runit) - sudo sv stop iofog-agent 2>/dev/null || true - ;; - upstart) - sudo initctl stop iofog-agent 2>/dev/null || true - ;; - *) - sudo systemctl stop iofog-agent 2>/dev/null || sudo service iofog-agent stop 2>/dev/null || true - ;; - esac - # Ensure container is stopped by name (in case init did not) - (docker stop ${AGENT_CONTAINER_NAME} 2>/dev/null || podman stop ${AGENT_CONTAINER_NAME} 2>/dev/null) || true -} - - - -do_create_env() { -ENV_FILE_NAME=iofog-agent.env # Used as an env file in systemd - -ENV_FILE="$ETC_DIR/$ENV_FILE_NAME" - -# Env file (for systemd) -rm -f "$ENV_FILE" -touch "$ENV_FILE" - -echo "IOFOG_AGENT_IMAGE=${agent_image}" >> "$ENV_FILE" -echo "IOFOG_AGENT_TZ=${agent_tz}" >> "$ENV_FILE" - -} - -do_install_iofog() { - echo "# Installing ioFog agent..." - - # 1. Ensure folders exist - for FOLDER in ${ETC_DIR} ${AGENT_LOG_FOLDER} ${AGENT_BACKUP_FOLDER} ${AGENT_MESSAGE_FOLDER} ${AGENT_SHARE_FOLDER}; do - if [ ! -d "$FOLDER" ]; then - echo "Creating folder: $FOLDER" - sudo mkdir -p "$FOLDER" - sudo chmod 775 "$FOLDER" - fi - done - do_create_env - - # Determine container engine (Podman for rpm-like distros, else Docker) - USE_PODMAN="false" - case "$lsb_dist" in - rhel|centos|fedora|ol|sles|opensuse*) USE_PODMAN="true" ;; - esac - if [ "$USE_PODMAN" = "true" ]; then - CONTAINER_RUNTIME="podman" - SOCK_MOUNT="-v /run/podman/podman.sock:/run/podman/podman.sock:rw" - else - CONTAINER_RUNTIME="docker" - SOCK_MOUNT="-v /var/run/docker.sock:/var/run/docker.sock:rw" - fi - - # Systemd: use Quadlet for Podman or systemd unit for Docker - if [ "${INIT_SYSTEM:-systemd}" = "systemd" ]; then - if [ "$USE_PODMAN" = "true" ]; then - echo "Using Podman (Quadlet) for container management..." - SYSTEMD_SERVICE_FILE=/etc/containers/systemd/iofog-agent.container - cat < /dev/null -[Unit] -Description=ioFog Agent Service -After=podman.service -Requires=podman.service - -[Container] -ContainerName=${AGENT_CONTAINER_NAME} -Image=${agent_image} -PodmanArgs=--privileged --stop-timeout=60 -EnvironmentFile=${ETC_DIR}/iofog-agent.env -Network=host -Volume=/run/podman/podman.sock:/run/podman/podman.sock:rw -Volume=iofog-agent-config:/etc/iofog-agent:rw -Volume=/var/log/iofog-agent:/var/log/iofog-agent:rw -Volume=/var/backups/iofog-agent:/var/backups/iofog-agent:rw -Volume=/usr/share/iofog-agent:/usr/share/iofog-agent:rw -Volume=/var/lib/iofog-agent:/var/lib/iofog-agent:rw -LogDriver=journald - -[Service] -Restart=always - -[Install] -WantedBy=default.target -EOF - sudo systemctl daemon-reload - sudo systemctl restart podman 2>/dev/null || true - sudo systemctl enable iofog-agent.service - sudo systemctl start iofog-agent.service - else - echo "Using Docker (systemd) for container management..." - SYSTEMD_SERVICE_FILE=/etc/systemd/system/iofog-agent.service - cat < /dev/null -[Unit] -Description=ioFog Agent Service -After=docker.service -Requires=docker.service - -[Service] -Restart=always -ExecStartPre=-/usr/bin/docker rm -f ${AGENT_CONTAINER_NAME} -ExecStart=/usr/bin/docker run --rm --name ${AGENT_CONTAINER_NAME} \\ ---env-file ${ETC_DIR}/iofog-agent.env \\ --v /var/run/docker.sock:/var/run/docker.sock:rw \\ --v iofog-agent-config:/etc/iofog-agent:rw \\ --v /var/log/iofog-agent:/var/log/iofog-agent:rw \\ --v /var/backups/iofog-agent:/var/backups/iofog-agent:rw \\ --v /usr/share/iofog-agent:/usr/share/iofog-agent:rw \\ --v /var/lib/iofog-agent:/var/lib/iofog-agent:rw \\ ---net=host \\ ---privileged \\ ---stop-timeout 60 \\ ---attach stdout \\ ---attach stderr \\ -${agent_image} -ExecStop=/usr/bin/docker stop ${AGENT_CONTAINER_NAME} - -[Install] -WantedBy=default.target -EOF - sudo systemctl daemon-reload - sudo systemctl enable iofog-agent.service - sudo systemctl start iofog-agent.service - fi - else - # Non-systemd: create init script that runs the container - echo "Using $CONTAINER_RUNTIME with $INIT_SYSTEM for container management..." - RUN_CMD="${CONTAINER_RUNTIME} run --rm -d --name ${AGENT_CONTAINER_NAME} --env-file ${ETC_DIR}/iofog-agent.env ${SOCK_MOUNT} -v iofog-agent-config:/etc/iofog-agent:rw -v /var/log/iofog-agent:/var/log/iofog-agent:rw -v /var/backups/iofog-agent:/var/backups/iofog-agent:rw -v /usr/share/iofog-agent:/usr/share/iofog-agent:rw -v /var/lib/iofog-agent:/var/lib/iofog-agent:rw --net=host --privileged --stop-timeout 60 ${agent_image}" - RUN_CMD_FG="${CONTAINER_RUNTIME} run --rm --name ${AGENT_CONTAINER_NAME} --env-file ${ETC_DIR}/iofog-agent.env ${SOCK_MOUNT} -v iofog-agent-config:/etc/iofog-agent:rw -v /var/log/iofog-agent:/var/log/iofog-agent:rw -v /var/backups/iofog-agent:/var/backups/iofog-agent:rw -v /usr/share/iofog-agent:/usr/share/iofog-agent:rw -v /var/lib/iofog-agent:/var/lib/iofog-agent:rw --net=host --privileged --stop-timeout 60 ${agent_image}" - STOP_CMD="${CONTAINER_RUNTIME} stop ${AGENT_CONTAINER_NAME}" - - case "$INIT_SYSTEM" in - sysvinit|openrc) - sudo tee /etc/init.d/iofog-agent > /dev/null </dev/null | grep -q "^${AGENT_CONTAINER_NAME}\$"; then exit 0; fi - $RUN_CMD - ;; - stop) - $STOP_CMD 2>/dev/null || true - ;; - restart) - \$0 stop; \$0 start - ;; - status) - if ${CONTAINER_RUNTIME} ps --format '{{.Names}}' 2>/dev/null | grep -q "^${AGENT_CONTAINER_NAME}\$"; then echo "running"; exit 0; else echo "stopped"; exit 1; fi - ;; - *) - echo "Usage: \$0 {start|stop|restart|status}" - exit 1 - ;; -esac -exit 0 -INITSCRIPT - sudo chmod +x /etc/init.d/iofog-agent - if [ "$INIT_SYSTEM" = "openrc" ]; then - sudo rc-update add iofog-agent default 2>/dev/null || true - sudo rc-service iofog-agent start - else - sudo update-rc.d iofog-agent defaults 2>/dev/null || sudo chkconfig iofog-agent on 2>/dev/null || true - sudo service iofog-agent start 2>/dev/null || sudo /etc/init.d/iofog-agent start - fi - ;; - s6) - sudo mkdir -p /etc/s6/sv/iofog-agent - sudo tee /etc/s6/sv/iofog-agent/run > /dev/null </dev/null || true - sudo s6-svc -u /etc/s6/sv/iofog-agent 2>/dev/null || true - ;; - runit) - sudo mkdir -p /etc/runit/sv/iofog-agent - sudo tee /etc/runit/sv/iofog-agent/run > /dev/null </dev/null || true - elif [ -d /etc/runit/runsvdir/default ]; then - sudo ln -sf /etc/runit/sv/iofog-agent /etc/runit/runsvdir/default/iofog-agent 2>/dev/null || true - fi - sudo sv start iofog-agent 2>/dev/null || true - ;; - upstart) - sudo tee /etc/init/iofog-agent.conf > /dev/null </dev/null || true - sudo initctl start iofog-agent 2>/dev/null || true - ;; - *) - echo "Warning: Unknown init system $INIT_SYSTEM. Creating /etc/init.d/iofog-agent fallback." - sudo tee /etc/init.d/iofog-agent > /dev/null < /dev/null -#!/bin/sh -CONTAINER_NAME="iofog-agent" -if ! podman ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "Error: The iofog-agent container is not running." - exit 1 -fi -exec podman exec ${CONTAINER_NAME} iofog-agent "$@" -EOF - else - cat <<'EOF' | sudo tee ${EXECUTABLE_FILE} > /dev/null -#!/bin/sh -CONTAINER_NAME="iofog-agent" -if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "Error: The iofog-agent container is not running." - exit 1 -fi -exec docker exec ${CONTAINER_NAME} iofog-agent "$@" -EOF - fi - sudo chmod +x ${EXECUTABLE_FILE} - - echo "ioFog agent installation completed!" -} - -do_start_iofog(){ - case "${INIT_SYSTEM:-systemd}" in - systemd) - sudo systemctl start iofog-agent >/dev/null 2>&1 & - ;; - sysvinit|openrc) - sudo service iofog-agent start 2>/dev/null || sudo /etc/init.d/iofog-agent start 2>/dev/null & - ;; - s6) - sudo s6-svc -u /etc/s6/sv/iofog-agent 2>/dev/null & - ;; - runit) - sudo sv start iofog-agent 2>/dev/null & - ;; - upstart) - sudo initctl start iofog-agent 2>/dev/null & - ;; - *) - sudo systemctl start iofog-agent 2>/dev/null || sudo /etc/init.d/iofog-agent start 2>/dev/null & - ;; - esac - local STATUS="" - local ITER=0 - while [ "$STATUS" != "RUNNING" ]; do - ITER=$((ITER+1)) - if [ "$ITER" -gt 600 ]; then - echo "Timed out waiting for Agent to be RUNNING" - exit 1 - fi - sleep 1 - STATUS=$(sudo iofog-agent status 2>/dev/null | cut -f2 -d: | head -n 1 | tr -d '[:space:]') - echo "${STATUS}" - done - sudo iofog-agent "config -cf 10 -sf 10" - if [ "$lsb_dist" = "rhel" ] || [ "$lsb_dist" = "centos" ] || [ "$lsb_dist" = "fedora" ] || [ "$lsb_dist" = "ol" ] || [ "$lsb_dist" = "sles" ] || [ "$lsb_dist" = "opensuse" ]; then - sudo iofog-agent "config -c unix:///var/run/podman/podman.sock" - fi -} - -agent_image="$1" -agent_tz="$2" -echo "Using variables" -echo "version: $agent_image" -echo "timezone: $agent_tz" -. /etc/iofog/agent/init.sh -init -do_check_install -do_stop_iofog -do_install_iofog -do_start_iofog \ No newline at end of file diff --git a/assets/container-agent/uninstall_iofog.sh b/assets/container-agent/uninstall_iofog.sh deleted file mode 100755 index fcde86ff8..000000000 --- a/assets/container-agent/uninstall_iofog.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/bin/sh -set -x -set -e - -AGENT_CONFIG_FOLDER=iofog-agent-config -AGENT_LOG_FOLDER=/var/log/iofog-agent -AGENT_BACKUP_FOLDER=/var/backups/iofog-agent -AGENT_MESSAGE_FOLDER=/var/lib/iofog-agent -EXECUTABLE_FILE=/usr/local/bin/iofog-agent -CONTAINER_NAME="iofog-agent" - -do_uninstall_iofog() { - echo "# Removing ioFog agent..." - - case "$lsb_dist" in - rhel|fedora|centos|ol|sles|opensuse*) CONTAINER_RUNTIME="podman" ;; - *) CONTAINER_RUNTIME="docker" ;; - esac - - # Stop and remove service based on init system - case "${INIT_SYSTEM:-systemd}" in - systemd) - for f in /etc/systemd/system/iofog-agent.service /etc/containers/systemd/iofog-agent.container; do - if [ -f "$f" ]; then - echo "Disabling and stopping systemd service..." - sudo systemctl stop iofog-agent.service 2>/dev/null || true - sudo systemctl disable iofog-agent.service 2>/dev/null || true - sudo rm -f "$f" - sudo systemctl daemon-reload - break - fi - done - ;; - sysvinit|openrc) - if [ -f /etc/init.d/iofog-agent ]; then - sudo service iofog-agent stop 2>/dev/null || sudo /etc/init.d/iofog-agent stop 2>/dev/null || true - [ "$INIT_SYSTEM" = "openrc" ] && sudo rc-update del iofog-agent default 2>/dev/null || true - sudo update-rc.d -f iofog-agent remove 2>/dev/null || sudo chkconfig --del iofog-agent 2>/dev/null || true - sudo rm -f /etc/init.d/iofog-agent - fi - ;; - s6) - sudo s6-svc -d /etc/s6/sv/iofog-agent 2>/dev/null || true - sudo rm -rf /etc/s6/sv/iofog-agent - [ -L /etc/s6/adminsv/default/iofog-agent ] && sudo rm -f /etc/s6/adminsv/default/iofog-agent - ;; - runit) - sudo sv stop iofog-agent 2>/dev/null || true - [ -L /var/service/iofog-agent ] && sudo rm -f /var/service/iofog-agent - [ -L /etc/runit/runsvdir/default/iofog-agent ] && sudo rm -f /etc/runit/runsvdir/default/iofog-agent - sudo rm -rf /etc/runit/sv/iofog-agent - ;; - upstart) - sudo initctl stop iofog-agent 2>/dev/null || true - sudo rm -f /etc/init/iofog-agent.conf - ;; - *) - sudo systemctl stop iofog-agent 2>/dev/null || true - sudo systemctl disable iofog-agent 2>/dev/null || true - sudo rm -f /etc/systemd/system/iofog-agent.service /etc/containers/systemd/iofog-agent.container - sudo systemctl daemon-reload 2>/dev/null || true - [ -f /etc/init.d/iofog-agent ] && sudo /etc/init.d/iofog-agent stop 2>/dev/null || true - sudo rm -f /etc/init.d/iofog-agent - ;; - esac - - # Remove the container - if sudo ${CONTAINER_RUNTIME} ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then - echo "Stopping and removing the ioFog agent container..." - sudo ${CONTAINER_RUNTIME} stop ${CONTAINER_NAME} 2>/dev/null || true - sudo ${CONTAINER_RUNTIME} rm ${CONTAINER_NAME} 2>/dev/null || true - fi - - # Remove config files - echo "Checking if the ${CONTAINER_RUNTIME} volume exists..." - - if sudo ${CONTAINER_RUNTIME} volume inspect "${AGENT_CONFIG_FOLDER}" >/dev/null 2>&1; then - echo "${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' found. Removing..." - sudo ${CONTAINER_RUNTIME} volume rm "${AGENT_CONFIG_FOLDER}" - echo "${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' has been removed." - else - echo "${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' does not exist. Skipping removal." - fi - - # Remove log files - echo "Removing log files..." - sudo rm -rf ${AGENT_LOG_FOLDER} - - # Remove backup files - echo "Removing backup files..." - sudo rm -rf ${AGENT_BACKUP_FOLDER} - - # Remove message files - echo "Removing message files..." - sudo rm -rf ${AGENT_MESSAGE_FOLDER} - - # Remove the executable script - if [ -f ${EXECUTABLE_FILE} ]; then - echo "Removing the iofog-agent executable script..." - sudo rm -f ${EXECUTABLE_FILE} - fi - - echo "ioFog agent uninstalled successfully!" -} - -. /etc/iofog/agent/init.sh -init - -do_uninstall_iofog diff --git a/assets/container-controller/check_prereqs.sh b/assets/container-controller/check_prereqs.sh deleted file mode 100755 index a6b148d4a..000000000 --- a/assets/container-controller/check_prereqs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -x - -# Check can sudo without password -if ! $(sudo ls /tmp/ > /dev/null); then - MSG="Unable to successfully use sudo with user $USER on this host.\nUser $USER must be in sudoers group and using sudo without password must be enabled.\nPlease see iofog.org documentation for more details." - echo $MSG - exit 1 -fi \ No newline at end of file diff --git a/assets/container-controller/init.sh b/assets/container-controller/init.sh deleted file mode 100755 index 630898c81..000000000 --- a/assets/container-controller/init.sh +++ /dev/null @@ -1,296 +0,0 @@ -#!/bin/sh -# Script to detect Linux distribution and version -# Used as a precursor for system-specific installations - -# Exit on error and print commands for debugging -set -e -set -x - -# Define user variable -user="$(id -un 2>/dev/null || true)" - -# Check if a command exists -command_exists() { - command -v "$@" > /dev/null 2>&1 -} - -# Detect the Linux distribution -get_distribution() { - lsb_dist="" - dist_version="" - - # Every system that we officially support has /etc/os-release - if [ -r /etc/os-release ]; then - - lsb_dist="$(. /etc/os-release && echo "$ID")" - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')" - else - echo "Error: Unsupported Linux distribution! /etc/os-release not found." - exit 1 - fi - - echo "# Detected distribution: $lsb_dist (version: $dist_version)" -} - -# Check if this is a forked Linux distro -check_forked() { - # Skip if lsb_release doesn't exist - if ! command_exists lsb_release; then - return - fi - - # Check if the `-u` option is supported - set +e - lsb_release -a > /dev/null 2>&1 - lsb_release_exit_code=$? - set -e - - # Check if the command has exited successfully, it means we're in a forked distro - if [ "$lsb_release_exit_code" = "0" ]; then - # Get the upstream release info - current_lsb_dist=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]') - current_dist_version=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]') - - # Print info about current distro - echo "You're using '$current_lsb_dist' version '$current_dist_version'." - - # Check if current is different from detected (indicating a fork) - if [ "$current_lsb_dist" != "$lsb_dist" ] || [ "$current_dist_version" != "$dist_version" ]; then - echo "Upstream release is '$lsb_dist' version '$dist_version'." - fi - else - # Additional checks for specific distros that might not be properly detected - if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then - if [ "$lsb_dist" = "osmc" ]; then - # OSMC runs Raspbian - lsb_dist=raspbian - else - # We're Debian and don't even know it! - lsb_dist=debian - fi - # Get Debian version and map it to codename - dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" - case "$dist_version" in - 14) - dist_version="forky" - ;; - 13) - dist_version="trixie" - ;; - 12) - dist_version="bookworm" - ;; - 11) - dist_version="bullseye" - ;; - 10) - dist_version="buster" - ;; - 9) - dist_version="stretch" - ;; - 8|'Kali Linux 2') - dist_version="jessie" - ;; - 7) - dist_version="wheezy" - ;; - esac - elif [ -r /etc/redhat-release ] && [ -z "$lsb_dist" ]; then - lsb_dist=redhat - # Extract version from redhat-release file - dist_version="$(sed 's/.*release \([0-9.]*\).*/\1/' /etc/redhat-release)" - fi - fi -} - -# Set up sudo command if necessary -setup_sudo() { - sh_c='sh -c' - if [ "$user" != 'root' ]; then - if command_exists sudo; then - sh_c='sudo -E sh -c' - elif command_exists su; then - sh_c='su -c' - else - echo "Error: this installer needs the ability to run commands as root." - echo "We are unable to find either 'sudo' or 'su' available to make this happen." - exit 1 - fi - fi - echo "# Using command executor: $sh_c" -} - -# Refine distribution version detection based on the distro -refine_distribution_version() { - case "$lsb_dist" in - ubuntu) - if command_exists lsb_release; then - dist_version="$(lsb_release --codename | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then - - dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")" - fi - ;; - - debian|raspbian) - # If we only have a number, map it to a codename for better recognition - if echo "$dist_version" | grep -qE '^[0-9]+$'; then - case "$dist_version" in - 14) - dist_version="forky" - ;; - 13) - dist_version="trixie" - ;; - 12) - dist_version="bookworm" - ;; - 11) - dist_version="bullseye" - ;; - 10) - # Handle special case for Buster - dist_version="buster" - if [ "$user" = 'root' ]; then - apt-get update --allow-releaseinfo-change || true - elif command_exists sudo; then - sudo apt-get update --allow-releaseinfo-change || true - fi - ;; - 9) - dist_version="stretch" - ;; - 8) - dist_version="jessie" - ;; - 7) - dist_version="wheezy" - ;; - esac - fi - ;; - - centos|rhel|fedora|ol) - # Make sure we have a version number - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - if [ -z "$dist_version" ] && [ -r /etc/redhat-release ]; then - dist_version="$(sed 's/.*release \([0-9.]*\).*/\1/' /etc/redhat-release)" - fi - ;; - - sles|opensuse) - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - # Fallback for older versions - if [ -z "$dist_version" ] && [ -r /etc/SuSE-release ]; then - dist_version="$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = //')" - fi - # Ensure version is in the correct format (e.g., 15.4 for SLES 15 SP4) - if [ -n "$dist_version" ]; then - # Remove any non-numeric characters except dots - dist_version="$(echo "$dist_version" | sed 's/[^0-9.]//g')" - fi - # Normalize distribution name - if [ "$lsb_dist" = "sles" ]; then - lsb_dist="sles" - elif [ "$lsb_dist" = "opensuse" ]; then - lsb_dist="opensuse" - fi - ;; - - *) - if command_exists lsb_release; then - dist_version="$(lsb_release --release | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - ;; - esac -} - -# Detect init system -detect_init_system() { - if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then - INIT_SYSTEM="systemd" - elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then - INIT_SYSTEM="upstart" - elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then - INIT_SYSTEM="openrc" - elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then - INIT_SYSTEM="s6" - elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then - INIT_SYSTEM="runit" - elif [ -d /etc/init.d ]; then - INIT_SYSTEM="sysvinit" - else - INIT_SYSTEM="unknown" - fi - export INIT_SYSTEM - echo "# Detected init system: $INIT_SYSTEM" -} - -# Detect package type (deb, rpm, or other) -detect_package_type() { - case "$lsb_dist" in - debian|ubuntu|raspbian|mendel) - PACKAGE_TYPE="deb" - ;; - fedora|centos|rhel|ol|sles|opensuse*) - PACKAGE_TYPE="rpm" - ;; - alpine) - PACKAGE_TYPE="apk" - ;; - *) - if command_exists apt-get || command_exists dpkg; then - PACKAGE_TYPE="deb" - elif command_exists yum || command_exists dnf || command_exists zypper; then - PACKAGE_TYPE="rpm" - elif command_exists apk; then - PACKAGE_TYPE="apk" - else - PACKAGE_TYPE="other" - fi - ;; - esac - export PACKAGE_TYPE - echo "# Detected package type: $PACKAGE_TYPE" -} - -# Init function -init() { - # Detect basic distribution info - get_distribution - - # Set up sudo for privileged commands - setup_sudo - - # Refine version information - refine_distribution_version - - # Check if this is a forked distro - check_forked - - # Detect init system and package type (for universal OS/init support) - detect_init_system - detect_package_type - - # Print final distribution information - echo "----------------------------------------" - echo "Linux Distribution: $lsb_dist" - echo "Version: $dist_version" - echo "Init system: $INIT_SYSTEM" - echo "Package type: $PACKAGE_TYPE" - echo "----------------------------------------" - -} diff --git a/assets/container-controller/install_container_engine.sh b/assets/container-controller/install_container_engine.sh deleted file mode 100755 index c0e2f9c44..000000000 --- a/assets/container-controller/install_container_engine.sh +++ /dev/null @@ -1,302 +0,0 @@ -#!/bin/sh -# Script to install Docker/Podman based on Linux distribution -# Sources init.sh for distribution detection - -set -x -set -e - -CONTAINER_ENGINE_MSG="This operating system does not support automatic container engine installation. Please install Docker 25+ or Podman 4+ on the target host and re-run, or use an airgap deployment with a pre-installed engine." - -check_docker_version() { - docker_version_num=0 - if command -v docker >/dev/null 2>&1; then - raw=$(docker -v 2>/dev/null | sed 's/.*version \([^,]*\),.*/\1/' | tr -d '.') - [ -n "$raw" ] && docker_version_num="$raw" - fi - [ "$docker_version_num" -ge 2500 ] 2>/dev/null || return 1 -} - -check_podman_version() { - podman_version_num=0 - if command -v podman >/dev/null 2>&1; then - raw=$(podman --version 2>/dev/null | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p') - [ -n "$raw" ] && podman_version_num="$raw" - fi - [ "$podman_version_num" -ge 4 ] 2>/dev/null || return 1 -} - -start_docker() { - set +e - if $sh_c "docker ps" >/dev/null 2>&1; then - set -e - return 0 - fi - err_code=1 - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl start docker" >/dev/null 2>&1 - err_code=$? - ;; - sysvinit) - $sh_c "service docker start" >/dev/null 2>&1 || $sh_c "/etc/init.d/docker start" >/dev/null 2>&1 - err_code=$? - ;; - openrc) - $sh_c "rc-service docker start" >/dev/null 2>&1 - err_code=$? - ;; - *) - $sh_c "/etc/init.d/docker start" >/dev/null 2>&1 - err_code=$? - [ $err_code -ne 0 ] && $sh_c "systemctl start docker" >/dev/null 2>&1 && err_code=0 - [ $err_code -ne 0 ] && $sh_c "service docker start" >/dev/null 2>&1 && err_code=0 - [ $err_code -ne 0 ] && $sh_c "snap start docker" >/dev/null 2>&1 && err_code=0 - ;; - esac - set -e - if [ $err_code -ne 0 ]; then - echo "Could not start Docker daemon" - exit 1 - fi -} - -start_podman() { - set +e - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl start podman" >/dev/null 2>&1 - $sh_c "systemctl start podman.socket" >/dev/null 2>&1 - ;; - sysvinit) - $sh_c "service podman start" >/dev/null 2>&1 || $sh_c "/etc/init.d/podman start" >/dev/null 2>&1 - ;; - openrc) - $sh_c "rc-service podman start" >/dev/null 2>&1 - ;; - *) - $sh_c "systemctl start podman" >/dev/null 2>&1 || true - $sh_c "systemctl start podman.socket" >/dev/null 2>&1 || true - $sh_c "service podman start" >/dev/null 2>&1 || true - ;; - esac - set -e -} - - -do_modify_daemon() { - # Skip for Podman installations - if [ "$USE_PODMAN" = "true" ]; then - echo "# Configuring Podman for CDI directory support..." - - # Create CDI directories - $sh_c "mkdir -p /etc/cdi /var/run/cdi" - - # Ensure /etc/containers exists - $sh_c "mkdir -p /etc/containers" - - # Create containers.conf if it doesn't exist - if [ ! -f "/etc/containers/containers.conf" ]; then - $sh_c 'cat > /etc/containers/containers.conf <> /etc/containers/containers.conf' - fi - fi - - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl enable podman" 2>/dev/null || true - $sh_c "systemctl enable podman.socket" 2>/dev/null || true - ;; - openrc) - $sh_c "rc-update add podman default" 2>/dev/null || true - ;; - sysvinit) - $sh_c "update-rc.d podman defaults" 2>/dev/null || $sh_c "chkconfig podman on" 2>/dev/null || true - ;; - *) ;; - esac - start_podman - return - fi - - # Original Docker daemon configuration - if [ ! -f /etc/docker/daemon.json ]; then - echo "Creating /etc/docker/daemon.json..." - $sh_c "mkdir -p /etc/docker" - $sh_c 'cat > /etc/docker/daemon.json << EOF -{ - "storage-driver": "overlayfs", - "features": { - "containerd-snapshotter": true, - "cdi": true - }, - "cdi-spec-dirs": ["/etc/cdi/", "/var/run/cdi"] -} -EOF' - else - echo "/etc/docker/daemon.json already exists" - fi - echo "Restarting Docker daemon..." - case "${INIT_SYSTEM:-unknown}" in - systemd) - $sh_c "systemctl daemon-reload" - $sh_c "systemctl restart docker" - ;; - *) - $sh_c "systemctl daemon-reload" 2>/dev/null || true - $sh_c "systemctl restart docker" 2>/dev/null || start_docker - ;; - esac -} - -do_install_container_engine() { - if [ "$PACKAGE_TYPE" = "apk" ]; then - if command_exists docker && check_docker_version; then - echo "# Docker already installed (>= 25)" - start_docker - do_modify_daemon - return 0 - fi - echo "# Installing Docker on Alpine..." - $sh_c "apk add docker" - $sh_c "rc-update add docker default" - $sh_c "service docker start" - $sh_c "addgroup $user docker" - if ! command_exists docker; then - echo "Failed to install Docker" - exit 1 - fi - if ! check_docker_version; then - echo "Error: Docker 25+ is required. Please upgrade the Docker package or install Docker 25+ manually." - exit 1 - fi - start_docker - do_modify_daemon - return 0 - fi - - if [ "$PACKAGE_TYPE" = "other" ]; then - if check_docker_version; then - USE_PODMAN="false" - echo "# Docker (>= 25) found; using Docker." - start_docker - do_modify_daemon - return 0 - fi - if check_podman_version; then - USE_PODMAN="true" - echo "# Podman (>= 4) found; using Podman." - do_modify_daemon - return 0 - fi - echo "Error: $CONTAINER_ENGINE_MSG" - exit 1 - fi - - if [ "$USE_PODMAN" = "true" ]; then - echo "# Installing Podman and related packages..." - case "$lsb_dist" in - fedora|centos|rhel|ol) - $sh_c "yum install -y podman crun podman-docker" - ;; - sles|opensuse*) - $sh_c "zypper install -y podman crun podman-docker" - ;; - esac - if ! check_podman_version; then - echo "Error: Podman 4+ is required. Please upgrade Podman." - exit 1 - fi - do_modify_daemon - return - fi - - if command_exists docker; then - docker_version=$(docker -v 2>/dev/null | sed 's/.*version \([^,]*\),.*/\1/' | tr -d '.') - if [ -n "$docker_version" ] && [ "$docker_version" -ge 2500 ] 2>/dev/null; then - echo "# Docker already installed (>= 25)" - start_docker - do_modify_daemon - return - fi - fi - - echo "# Installing Docker..." - case "$lsb_dist" in - debian|ubuntu|raspbian) - case "$dist_version" in - "stretch") - $sh_c "apt install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common" - curl -fsSL https://download.docker.com/linux/debian/gpg | $sh_c "apt-key add -" - $sh_c "add-apt-repository \"deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/debian $(lsb_release -cs) stable\"" - $sh_c "apt update -y" - $sh_c "apt install -y docker-ce" - ;; - *) - curl -fsSL https://get.docker.com/ | $sh_c "sh" - ;; - esac - ;; - *) - curl -fsSL https://get.docker.com/ | $sh_c "sh" - ;; - esac - - if ! command_exists docker; then - echo "Failed to install Docker" - exit 1 - fi - if ! check_docker_version; then - echo "Error: Docker 25+ is required. Please upgrade Docker." - exit 1 - fi - start_docker - do_modify_daemon -} - -# Check if we should use Podman based on distribution -determine_container_engine() { - USE_PODMAN="false" - case "$lsb_dist" in - fedora|centos|rhel|ol|sles|opensuse*) - USE_PODMAN="true" - echo "# Using Podman for $lsb_dist" - ;; - *) - echo "# Using Docker for $lsb_dist" - ;; - esac -} - -# Source init.sh to get distribution info -. /etc/iofog/controller/init.sh -init - -# Configure container engine based on distribution -determine_container_engine - -# Install appropriate container engine -do_install_container_engine - -echo "# Installation completed successfully" \ No newline at end of file diff --git a/assets/container-controller/install_iofog.sh b/assets/container-controller/install_iofog.sh deleted file mode 100755 index 71562d21d..000000000 --- a/assets/container-controller/install_iofog.sh +++ /dev/null @@ -1,232 +0,0 @@ -#!/bin/sh -set -x -set -e - -# INSTALL_DIR="/opt/iofog" -TMP_DIR="/tmp/iofog" -ETC_DIR="/etc/iofog/controller" -CONTROLLER_LOG_FOLDER=/var/log/iofog-controller -CONTROLLER_CONTAINER_NAME="iofog-controller" - -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -do_stop_iofog_controller() { - if ! command_exists iofog-controller; then - return 0 - fi - case "${INIT_SYSTEM:-systemd}" in - systemd) - sudo systemctl stop iofog-controller 2>/dev/null || true - ;; - sysvinit|openrc) - sudo service iofog-controller stop 2>/dev/null || sudo /etc/init.d/iofog-controller stop 2>/dev/null || true - ;; - s6) - sudo s6-svc -d /etc/s6/sv/iofog-controller 2>/dev/null || true - ;; - runit) - sudo sv stop iofog-controller 2>/dev/null || true - ;; - upstart) - sudo initctl stop iofog-controller 2>/dev/null || true - ;; - *) - sudo systemctl stop iofog-controller 2>/dev/null || sudo service iofog-controller stop 2>/dev/null || true - ;; - esac - (docker stop ${CONTROLLER_CONTAINER_NAME} 2>/dev/null || podman stop ${CONTROLLER_CONTAINER_NAME} 2>/dev/null) || true -} - -do_install_iofog_controller() { - echo "# Installing ioFog controller..." - - for FOLDER in ${ETC_DIR} ${CONTROLLER_LOG_FOLDER}; do - if [ ! -d "$FOLDER" ]; then - echo "Creating folder: $FOLDER" - sudo mkdir -p "$FOLDER" - sudo chmod 775 "$FOLDER" - fi - done - - USE_PODMAN="false" - case "$lsb_dist" in - rhel|centos|fedora|ol|sles|opensuse*) USE_PODMAN="true" ;; - esac - - CONTROLLER_RUN_ARGS="-e IOFOG_CONTROLLER_IMAGE=${controller_image} --env-file ${ETC_DIR}/iofog-controller.env -v iofog-controller-db:/home/runner/.npm-global/lib/node_modules/@eclipse-iofog/iofogcontroller/src/data/sqlite_files/:rw -v iofog-controller-log:/var/log/iofog-controller:rw -p 51121:51121 -p 80:8008 --stop-timeout 60 ${controller_image}" - - if [ "${INIT_SYSTEM:-systemd}" = "systemd" ]; then - if [ "$USE_PODMAN" = "true" ]; then - echo "Creating Quadlet container file for ioFog controller..." - sudo mkdir -p /etc/containers/systemd - cat < /dev/null -[Unit] -Description=ioFog Controller Service -After=podman.service -Requires=podman.service - -[Container] -ContainerName=${CONTROLLER_CONTAINER_NAME} -Image=${controller_image} -PodmanArgs=--stop-timeout=60 -Environment=IOFOG_CONTROLLER_IMAGE=${controller_image} -EnvironmentFile=${ETC_DIR}/iofog-controller.env -Volume=iofog-controller-db:/home/runner/.npm-global/lib/node_modules/@eclipse-iofog/iofogcontroller/src/data/sqlite_files/:rw -Volume=iofog-controller-log:/var/log/iofog-controller:rw -PublishPort=51121:51121 -PublishPort=80:8008 -LogDriver=journald - -[Service] -Restart=always - -[Install] -WantedBy=default.target -EOF - sudo systemctl daemon-reload - sudo systemctl restart podman 2>/dev/null || true - sudo systemctl enable iofog-controller.service - sudo systemctl start iofog-controller.service - else - echo "Creating systemd service for ioFog controller..." - cat < /dev/null -[Unit] -Description=ioFog Controller Service -After=docker.service -Requires=docker.service - -[Service] -TimeoutStartSec=0 -Restart=always -ExecStartPre=-/usr/bin/docker rm -f ${CONTROLLER_CONTAINER_NAME} -ExecStart=/usr/bin/docker run --rm --name ${CONTROLLER_CONTAINER_NAME} \\ -${CONTROLLER_RUN_ARGS} -ExecStop=/usr/bin/docker stop ${CONTROLLER_CONTAINER_NAME} - -[Install] -WantedBy=default.target -EOF - sudo systemctl daemon-reload - sudo systemctl enable iofog-controller.service - sudo systemctl start iofog-controller.service - fi - else - if [ "$USE_PODMAN" = "true" ]; then - RUN_CMD="podman run --rm -d --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}" - RUN_CMD_FG="podman run --rm --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}" - else - RUN_CMD="docker run --rm -d --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}" - RUN_CMD_FG="docker run --rm --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}" - fi - if [ "$USE_PODMAN" = "true" ]; then - STOP_CMD="podman stop ${CONTROLLER_CONTAINER_NAME}" - else - STOP_CMD="docker stop ${CONTROLLER_CONTAINER_NAME}" - fi - - case "$INIT_SYSTEM" in - sysvinit|openrc) - sudo tee /etc/init.d/iofog-controller > /dev/null </dev/null | grep -q "^${CONTROLLER_CONTAINER_NAME}\$"; then exit 0; fi - if podman ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTROLLER_CONTAINER_NAME}\$"; then exit 0; fi - $RUN_CMD - ;; - stop) $STOP_CMD 2>/dev/null || true ;; - restart) \$0 stop; \$0 start ;; - status) - if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTROLLER_CONTAINER_NAME}\$"; then echo "running"; exit 0; fi - if podman ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTROLLER_CONTAINER_NAME}\$"; then echo "running"; exit 0; fi - echo "stopped"; exit 1 - ;; - *) echo "Usage: \$0 {start|stop|restart|status}"; exit 1 ;; -esac -exit 0 -INITSCRIPT - sudo chmod +x /etc/init.d/iofog-controller - if [ "$INIT_SYSTEM" = "openrc" ]; then - sudo rc-update add iofog-controller default 2>/dev/null || true - sudo rc-service iofog-controller start - else - sudo update-rc.d iofog-controller defaults 2>/dev/null || sudo chkconfig iofog-controller on 2>/dev/null || true - sudo service iofog-controller start 2>/dev/null || sudo /etc/init.d/iofog-controller start - fi - ;; - s6) - sudo mkdir -p /etc/s6/sv/iofog-controller - printf '#!/bin/sh\nexec %s\n' "$RUN_CMD_FG" | sudo tee /etc/s6/sv/iofog-controller/run > /dev/null - sudo chmod +x /etc/s6/sv/iofog-controller/run - [ -d /etc/s6/adminsv/default ] && sudo ln -sf /etc/s6/sv/iofog-controller /etc/s6/adminsv/default/iofog-controller 2>/dev/null || true - sudo s6-svc -u /etc/s6/sv/iofog-controller 2>/dev/null || true - ;; - runit) - sudo mkdir -p /etc/runit/sv/iofog-controller - printf '#!/bin/sh\nexec %s\n' "$RUN_CMD_FG" | sudo tee /etc/runit/sv/iofog-controller/run > /dev/null - sudo chmod +x /etc/runit/sv/iofog-controller/run - [ -d /var/service ] && sudo ln -sf /etc/runit/sv/iofog-controller /var/service/iofog-controller 2>/dev/null || true - [ -d /etc/runit/runsvdir/default ] && sudo ln -sf /etc/runit/sv/iofog-controller /etc/runit/runsvdir/default/iofog-controller 2>/dev/null || true - sudo sv start iofog-controller 2>/dev/null || true - ;; - upstart) - printf 'description "IoFog Controller container"\nstart on runlevel [2345]\nstop on runlevel [!2345]\nrespawn\nrespawn limit 10 5\nexec %s\n' "$RUN_CMD_FG" | sudo tee /etc/init/iofog-controller.conf > /dev/null - sudo initctl reload-configuration 2>/dev/null || true - sudo initctl start iofog-controller 2>/dev/null || true - ;; - *) - sudo tee /etc/init.d/iofog-controller > /dev/null < /dev/null -#!/bin/sh -CONTAINER_NAME="iofog-controller" -if ! podman ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "Error: The iofog-controller container is not running." - exit 1 -fi -exec podman exec ${CONTAINER_NAME} iofog-controller "$@" -EOF - else - cat <<'EOF' | sudo tee ${EXECUTABLE_FILE} > /dev/null -#!/bin/sh -CONTAINER_NAME="iofog-controller" -if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "Error: The iofog-controller container is not running." - exit 1 -fi -exec docker exec ${CONTAINER_NAME} iofog-controller "$@" -EOF - fi - sudo chmod +x ${EXECUTABLE_FILE} - - echo "ioFog controller installation completed!" -} - -# main -controller_image="$1" - -. /etc/iofog/controller/init.sh -init -do_stop_iofog_controller -do_install_iofog_controller diff --git a/assets/container-controller/set_env.sh b/assets/container-controller/set_env.sh deleted file mode 100755 index 877e4a716..000000000 --- a/assets/container-controller/set_env.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -set -x -set -e - -ETC_DIR="/etc/iofog/controller" -ENV_FILE_NAME=iofog-controller.env # Used as an env file in systemd - -ENV_FILE="$ETC_DIR/$ENV_FILE_NAME" - -# Create folder -mkdir -p "$ETC_DIR" - -# Env file (for systemd) -rm -f "$ENV_FILE" -touch "$ENV_FILE" - -for var in "$@" -do - echo "$var" >> "$ENV_FILE" -done \ No newline at end of file diff --git a/assets/container-controller/uninstall_iofog.sh b/assets/container-controller/uninstall_iofog.sh deleted file mode 100644 index 3356c92f0..000000000 --- a/assets/container-controller/uninstall_iofog.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/sh -set -x -set -e - - -CONTROLLER_LOG_DIR="iofog-controller-log" -CONTAINER_NAME="iofog-controller" -EXECUTABLE_FILE=/usr/local/bin/iofog-controller -CONTROLLER_DB=iofog-controller-db - - -do_uninstall_controller() { - echo "# Removing ioFog controller..." - - case "$lsb_dist" in - rhel|fedora|centos|ol|sles|opensuse*) CONTAINER_RUNTIME="podman" ;; - *) CONTAINER_RUNTIME="docker" ;; - esac - - case "${INIT_SYSTEM:-systemd}" in - systemd) - for f in /etc/systemd/system/iofog-controller.service /etc/containers/systemd/iofog-controller.container; do - if [ -f "$f" ]; then - echo "Disabling and stopping systemd service..." - sudo systemctl stop iofog-controller.service 2>/dev/null || true - sudo systemctl disable iofog-controller.service 2>/dev/null || true - sudo rm -f "$f" - sudo systemctl daemon-reload - break - fi - done - ;; - sysvinit|openrc) - if [ -f /etc/init.d/iofog-controller ]; then - sudo service iofog-controller stop 2>/dev/null || sudo /etc/init.d/iofog-controller stop 2>/dev/null || true - [ "$INIT_SYSTEM" = "openrc" ] && sudo rc-update del iofog-controller default 2>/dev/null || true - sudo update-rc.d -f iofog-controller remove 2>/dev/null || sudo chkconfig --del iofog-controller 2>/dev/null || true - sudo rm -f /etc/init.d/iofog-controller - fi - ;; - s6) - sudo s6-svc -d /etc/s6/sv/iofog-controller 2>/dev/null || true - sudo rm -rf /etc/s6/sv/iofog-controller - [ -L /etc/s6/adminsv/default/iofog-controller ] && sudo rm -f /etc/s6/adminsv/default/iofog-controller - ;; - runit) - sudo sv stop iofog-controller 2>/dev/null || true - [ -L /var/service/iofog-controller ] && sudo rm -f /var/service/iofog-controller - [ -L /etc/runit/runsvdir/default/iofog-controller ] && sudo rm -f /etc/runit/runsvdir/default/iofog-controller - sudo rm -rf /etc/runit/sv/iofog-controller - ;; - upstart) - sudo initctl stop iofog-controller 2>/dev/null || true - sudo rm -f /etc/init/iofog-controller.conf - ;; - *) - sudo systemctl stop iofog-controller 2>/dev/null || true - sudo systemctl disable iofog-controller 2>/dev/null || true - sudo rm -f /etc/systemd/system/iofog-controller.service /etc/containers/systemd/iofog-controller.container - sudo systemctl daemon-reload 2>/dev/null || true - [ -f /etc/init.d/iofog-controller ] && sudo /etc/init.d/iofog-controller stop 2>/dev/null || true - sudo rm -f /etc/init.d/iofog-controller - ;; - esac - - if sudo ${CONTAINER_RUNTIME} ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then - echo "Stopping and removing the ioFog controller container..." - sudo ${CONTAINER_RUNTIME} stop ${CONTAINER_NAME} 2>/dev/null || true - sudo ${CONTAINER_RUNTIME} rm ${CONTAINER_NAME} 2>/dev/null || true - fi - - # Remove config files - echo "Checking if the ${CONTAINER_RUNTIME} volume exists..." - - if sudo ${CONTAINER_RUNTIME} volume inspect "${CONTROLLER_DB}" >/dev/null 2>&1; then - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' found. Removing..." - sudo ${CONTAINER_RUNTIME} volume rm "${CONTROLLER_DB}" - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' has been removed." - else - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' does not exist. Skipping removal." - fi - - # Remove log files - echo "Removing log files..." - if sudo ${CONTAINER_RUNTIME} volume inspect "${CONTROLLER_LOG_DIR}" >/dev/null 2>&1; then - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' found. Removing..." - sudo ${CONTAINER_RUNTIME} volume rm "${CONTROLLER_LOG_DIR}" - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' has been removed." - else - echo "${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' does not exist. Skipping removal." - fi - - - # Remove the executable script - if [ -f ${EXECUTABLE_FILE} ]; then - echo "Removing the iofog-controller executable script..." - sudo rm -f ${EXECUTABLE_FILE} - fi - - echo "ioFog controller uninstalled successfully!" -} - -. /etc/iofog/controller/init.sh -init - -do_uninstall_controller \ No newline at end of file diff --git a/assets/controller/check_prereqs.sh b/assets/controller/check_prereqs.sh deleted file mode 100755 index a6b148d4a..000000000 --- a/assets/controller/check_prereqs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -x - -# Check can sudo without password -if ! $(sudo ls /tmp/ > /dev/null); then - MSG="Unable to successfully use sudo with user $USER on this host.\nUser $USER must be in sudoers group and using sudo without password must be enabled.\nPlease see iofog.org documentation for more details." - echo $MSG - exit 1 -fi \ No newline at end of file diff --git a/assets/controller/install_iofog.sh b/assets/controller/install_iofog.sh deleted file mode 100755 index 72763850f..000000000 --- a/assets/controller/install_iofog.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/bin/sh -set -x -set -e - -INSTALL_DIR="/opt/iofog" -TMP_DIR="/tmp/iofog" -ETC_DIR="/etc/iofog/controller" - -controller_service() { - USE_SYSTEMD=`grep -m1 -c systemd /proc/1/comm` - USE_INITCTL=`which initctl | wc -l` - USE_SERVICE=`which service | wc -l` - - if [ $USE_SYSTEMD -eq 1 ]; then - cp "$ETC_DIR/service/iofog-controller.systemd" /etc/systemd/system/iofog-controller.service - chmod 644 /etc/systemd/system/iofog-controller.service - systemctl daemon-reload - systemctl enable iofog-controller.service - elif [ $USE_INITCTL -eq 1 ]; then - cp "$ETC_DIR/service/iofog-controller.initctl" /etc/init/iofog-controller.conf - initctl reload-configuration - elif [ $USE_SERVICE -eq 1 ]; then - cp "$ETC_DIR/service/iofog-controller.update-rc" /etc/init.d/iofog-controller - chmod +x /etc/init.d/iofog-controller - update-rc.d iofog-controller defaults - else - echo "Unable to setup Controller startup script." - fi -} - -install_package() { - if [ -z "$(command -v apt)" ]; then - echo "Unsupported distro" - exit 1 - fi - apt update -qq - apt install -y $1 -} - -install_deps() { - if [ -z "$(command -v curl)" ]; then - install_package "curl" - fi - - if [ -z "$(command -v lsof)" ]; then - install_package "lsof" - fi - - if [ -z "$(command -v make)" ]; then - install_package "build-essential" - fi - - if [ -z "$(command -v python2)" ]; then - install_package "python2" - fi - - if [ -z "$(command -v python3)" ]; then - install_package "python3" - fi - - if [ -z "$(command -v python-is-python3)" ]; then - install_package "python-is-python3" - fi -} - -create_logrotate() { - cat < /etc/logrotate.d/iofog-controller -/var/log/iofog-controller/iofog-controller.log { - rotate 10 - size 100m - compress - notifempty - missingok - postrotate - kill -HUP `cat $INSTALL_DIR/controller/lib/node_modules/@eclipse-iofog/iofogcontroller/src/iofog-controller.pid` -} -EOF - chmod 644 /etc/logrotate.d/iofog-controller -} - -deploy_controller() { - # Nuke any existing instances - if [ ! -z "$(lsof -ti tcp:51121)" ]; then - lsof -ti tcp:51121 | xargs kill - fi - -# # If token is provided, set up private repo -# if [ ! -z $token ]; then -# if [ ! -z $(npmrc | grep iofog) ]; then -# npmrc -c iofog -# npmrc iofog -# fi -# curl -s https://"$token":@packagecloud.io/install/repositories/"$repo"/script.node.sh?package_id=7463817 | force_npm=1 bash -# mv ~/.npmrc ~/.npmrcs/npmrc -# ln -s ~/.npmrcs/npmrc ~/.npmrc -# else -# npmrc default -# fi - # Save DB - if [ -f "$INSTALL_DIR/controller/lib/node_modules/@eclipse-iofog/iofogcontroller/package.json" ]; then - # If iofog-controller is not running, it will fail to stop - ignore that failure. - node $INSTALL_DIR/controller/lib/node_modules/@eclipse-iofog/iofogcontroller/scripts/scripts-api.js preuninstall > /dev/null 2>&1 || true - fi - - # Install in temporary location - mkdir -p "$TMP_DIR/controller" - chmod 0777 "$TMP_DIR/controller" - if [ -z $version ]; then - npm install -g -f @eclipse-iofog/iofogcontroller --unsafe-perm --prefix "$TMP_DIR/controller" - else - npm install -g -f @eclipse-iofog/iofogcontroller --unsafe-perm --prefix "$TMP_DIR/controller" - fi - # Move files into $INSTALL_DIR/controller - mkdir -p "$INSTALL_DIR/" - rm -rf "$INSTALL_DIR/controller" # Clean possible previous install - mv "$TMP_DIR/controller/" "$INSTALL_DIR/" - - # Restore DB - if [ -f "$INSTALL_DIR/controller/lib/node_modules/@eclipse-iofog/iofogcontroller/package.json" ]; then - node $INSTALL_DIR/controller/lib/node_modules/@eclipse-iofog/iofogcontroller/scripts/scripts-api.js postinstall > /dev/null 2>&1 || true - fi - - # Symbolic links - if [ ! -f "/usr/local/bin/iofog-controller" ]; then - ln -fFs "$INSTALL_DIR/controller/bin/iofog-controller" /usr/local/bin/iofog-controller - fi - - # Set controller permissions - chmod 744 -R "$INSTALL_DIR/controller" - - # Startup script - controller_service - - # Run controller - . /opt/iofog/config/controller/env.sh - iofog-controller start -} - -# main -version="$1" -# repo=$([ -z "$2" ] && echo "iofog/iofog-controller-snapshots" || echo "$2") -# token="$3" - -install_deps -create_logrotate -deploy_controller diff --git a/assets/controller/install_node.sh b/assets/controller/install_node.sh deleted file mode 100755 index 80288a035..000000000 --- a/assets/controller/install_node.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh -set -x -set -e - -load_existing_nvm() { - set +e - if [ -z "$(command -v nvm)" ]; then - export NVM_DIR="${HOME}/.nvm" - mkdir -p $NVM_DIR - if [ -f "$NVM_DIR/nvm.sh" ]; then - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm - fi - fi - set -e -} - -install_node() { - load_existing_nvm - if [ -z "$(command -v nvm)" ]; then - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/refs/tags/v0.40.1/install.sh | bash - export NVM_DIR="${HOME}/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - fi - nvm install v20.17.0 - nvm use v20.17.0 - ln -Ffs $(which node) /usr/local/bin/node - ln -Ffs $(which npm) /usr/local/bin/npm - - # npmrc - if [ -z "$(command -v npmrc)" ]; then - npm i npmrc -g - fi - ln -Ffs $(which npmrc) /usr/local/bin/npmrc -} - -install_node \ No newline at end of file diff --git a/assets/controller/service/iofog-controller.initctl b/assets/controller/service/iofog-controller.initctl deleted file mode 100644 index 44baaf66f..000000000 --- a/assets/controller/service/iofog-controller.initctl +++ /dev/null @@ -1,11 +0,0 @@ -description "ioFog Controller" - -start on (runlevel [2345]) -stop on (runlevel [!2345]) - -respawn - -script - . /opt/iofog/config/controller/env.sh - exec /usr/local/bin/iofog-controller start -end script \ No newline at end of file diff --git a/assets/controller/service/iofog-controller.systemd b/assets/controller/service/iofog-controller.systemd deleted file mode 100644 index 3694fcb44..000000000 --- a/assets/controller/service/iofog-controller.systemd +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=ioFog Controller - -[Service] -Type=forking -ExecStart=/usr/local/bin/iofog-controller start -ExecStop=/usr/local/bin/iofog-controller stop -EnvironmentFile=/opt/iofog/config/controller/env.env - -[Install] -WantedBy=multi-user.target diff --git a/assets/controller/service/iofog-controller.update-rc b/assets/controller/service/iofog-controller.update-rc deleted file mode 100644 index 33b7a2153..000000000 --- a/assets/controller/service/iofog-controller.update-rc +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -case "$1" in - start) - . /opt/iofog/controller/env.env - /usr/local/bin/iofog-controller start - ;; - stop) - /usr/local/bin/iofog-controller stop - ;; - restart) - /usr/local/bin/iofog-controller stop - . /opt/iofog/config/controller/env.sh - /usr/local/bin/iofog-controller start - ;; - *) - echo "Usage: $0 {start|stop|restart}" -esac diff --git a/assets/controller/set_env.sh b/assets/controller/set_env.sh deleted file mode 100755 index 0bbfa000d..000000000 --- a/assets/controller/set_env.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -set -x -set -e - -CONF_FOLDER=/opt/iofog/config/controller -SOURCE_FILE_NAME=env.sh # Used to source env variables -ENV_FILE_NAME=env.env # Used as an env file in systemd - -SOURCE_FILE="$CONF_FOLDER/$SOURCE_FILE_NAME" -ENV_FILE="$CONF_FOLDER/$ENV_FILE_NAME" - -# Create folder -mkdir -p "$CONF_FOLDER" - -# Source file -echo "#!/bin/sh" > "$SOURCE_FILE" - -# Env file (for systemd) -rm -f "$ENV_FILE" -touch "$ENV_FILE" - -for var in "$@" -do - echo "export $var" >> "$SOURCE_FILE" - echo "$var" >> "$ENV_FILE" -done \ No newline at end of file diff --git a/assets/controller/uninstall_iofog.sh b/assets/controller/uninstall_iofog.sh deleted file mode 100644 index e8f7d3884..000000000 --- a/assets/controller/uninstall_iofog.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh -set -x -set -e - -CONTROLLER_DIR="/opt/iofog/controller/" -CONTROLLER_LOG_DIR="/var/log/iofog/" - -do_uninstall_controller() { - # Remove folders - sudo rm -rf $CONTROLLER_DIR - sudo rm -rf $CONTROLLER_LOG_DIR - - # Remove symbolic links - rm -f /usr/local/bin/iofog-controller - - # Remove service files - USE_SYSTEMD=`grep -m1 -c systemd /proc/1/comm` - USE_INITCTL=`which initctl | wc -l` - USE_SERVICE=`which service | wc -l` - - if [ $USE_SYSTEMD -eq 1 ]; then - systemctl stop iofog-controller.service - rm -f /etc/systemd/system/iofog-controller.service - elif [ $USE_INITCTL -eq 1 ]; then - rm -f /etc/init/iofog-controller.conf - elif [ $USE_SERVICE -eq 1 ]; then - rm -f /etc/init.d/iofog-controller - else - echo "Unable to setup Controller startup script." - fi -} - -do_uninstall_controller \ No newline at end of file diff --git a/assets/edgelet/edgelet-config.yaml b/assets/edgelet/edgelet-config.yaml new file mode 100644 index 000000000..e3da23ada --- /dev/null +++ b/assets/edgelet/edgelet-config.yaml @@ -0,0 +1,49 @@ +# Edgelet release default configuration (sample). +# Copy to /etc/edgelet/config.yaml on first install, or run: edgelet init-config +currentProfile: production +profiles: + production: + controllerUrl: "http://localhost:54421/api/v3/" + iofogUuid: "" + secureMode: "on" + devMode: "off" + controllerCert: "/etc/edgelet/cert.crt" + arch: "auto" + networkInterface: "dynamic" + # containerEngine selects the container runtime. + # Supported values: + # docker — use Docker daemon (requires Docker installed on host). + # containerEngineUrl specifies the socket path. + # default containerEngineUrl is unix:///var/run/docker.sock + # podman — use Podman daemon via its Docker-compatible API. + # containerEngineUrl is used as the Podman socket path; if empty, + # the default Podman socket is auto-detected. + # default containerEngineUrl is unix:///run/podman/containerd.sock + # edgelet — default container engine for linux, uses the embedded containerd engine bundled with edgelet. + # containerEngineUrl is ignored; the daemon always connects to its private + # socket at /run/edgelet/containerd.sock. + # Persistent data lives in /var/lib/edgelet-containerd/ and + # is NOT counted against diskDirectory/diskConsumptionLimit. + containerEngine: "edgelet" + containerEngineUrl: "unix:///run/edgelet/containerd.sock" + diskConsumptionLimit: "10" + diskDirectory: "/var/lib/edgelet/" + memoryConsumptionLimit: "4096" + processorConsumptionLimit: "80.0" + logDiskConsumptionLimit: "10.0" + logDiskDirectory: "/var/log/edgelet/" + logFileCount: "10" + logLevel: "INFO" + statusFrequency: "10" + changeFrequency: "10" + scanDevicesFreq: "60" + gps: "auto" + gpsCoordinates: "0,0" + gpsDevice: "" + gpsScanFrequency: "60" + watchdogEnabled: "off" + edgeGuardFreq: "0" + pruningFrequency: "0" + availableDiskThreshold: "20" + upgradeScanFrequency: "24" + timeZone: "Europe/Istanbul" diff --git a/assets/edgelet/edgelet-controller-ca.crt b/assets/edgelet/edgelet-controller-ca.crt new file mode 100644 index 000000000..345be5021 --- /dev/null +++ b/assets/edgelet/edgelet-controller-ca.crt @@ -0,0 +1,3728 @@ +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI +T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz +MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF +SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh +bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z +xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ +spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 +58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR +at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll +5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq +nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK +V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ +pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO +z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn +jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ +WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF +7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli +awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u ++2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 +X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN +SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo +P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI ++pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz +znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 +eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 +YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy +r/6zcCwupvI= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw +CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ +VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy +MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ +TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS +b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B +IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ ++kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK +sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA +94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B +43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf +e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C +cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O +BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO +PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw +hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG +XSaQpYXFuXqUPoeovQA= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG +A1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw +FwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx +MDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u +aXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b +RatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z +YybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3 +QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw +yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+ +BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ +SaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH +r96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0 +4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me +dKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw +q7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2 +nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu +H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA +VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC +XtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd +6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf ++I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi +kvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7 +wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB +TI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C +MUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn +4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I +aFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy +qfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt +nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY +6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu +MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k +RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg +f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV ++3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo +dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa +G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq +gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H +vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC +B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u +NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg +yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev +HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 +xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR +TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg +JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV +7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl +6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy +NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy +cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N +2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 +TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C +tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR +QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD +YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 +MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM +vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b +rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk +ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z +O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R +tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS +jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh +sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho +mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu ++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR +i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT +kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 +zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG +5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 +qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP +AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk +gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs +YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 +9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome +/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 +J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 +wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy +BiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw +NTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3emhF +KxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mt +p7JIKwccJ/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zd +J1M3s6oYwlkm7Fsf0uZlfO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gur +FzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBFEaCeVESE99g2zvVQR9wsMJvuwPWW0v4J +hscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1UefNzFJM3IFTQy2VYzxV4+K +h9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsF +AAOCAQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6Ld +mmQOmFxv3Y67ilQiLUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJ +mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA +8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV +55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/ +yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe +Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU +cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS +T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK +AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 +nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep +qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA +yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs +hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX +zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv +kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT +f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA +uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih +MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 +wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 +XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 +JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j +ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV +VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx +xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on +AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d +7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj +gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV ++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo +FGWsJwt0ivKH +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y +MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz +dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx +s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw +LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD +pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE +AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR +UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj +/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw +WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw +NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE +ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB +c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ +AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp +guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw +DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 +L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR +OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM +BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN +MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG +A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 +c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ +NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ +Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 +HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 +ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb +xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX +i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ +UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j +TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT +bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 +S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 +iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt +7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp +2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ +g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj +pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M +pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP +XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe +SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 +ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy +323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU +TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow +dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy +b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T +emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS +AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v +SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K +ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI +zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt +y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl +C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 +uWWL +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/assets/edgelet/scripts/bundled.sh b/assets/edgelet/scripts/bundled.sh new file mode 100644 index 000000000..9063ea8fd --- /dev/null +++ b/assets/edgelet/scripts/bundled.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Publish potctl edgelet script bundle into the canonical OS share directory. +set -e + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/lib/common.sh" +. "$SCRIPT_DIR/lib/paths.sh" + +EDGELET_DETECT_SOURCED=1 +. "$SCRIPT_DIR/detect_init.sh" +init +init_platform_paths "$EDGELET_OS" + +if desktop_container_local; then + info "Skipping bundled publish on desktop local container deploy" + exit 0 +fi + +_share="$(share_dir_for_os "$EDGELET_OS")" +maybe_sudo mkdir -p "$_share" "$_share/lib" +maybe_sudo chmod 755 "$_share" "$_share/lib" 2>/dev/null || true + +_publish() { + _src="$1" + _name="$2" + [ -f "$_src" ] || return 0 + maybe_sudo install -m 755 "$_src" "$_share/$_name" +} + +for _script in \ + check_prereqs.sh \ + detect_init.sh \ + install_deps.sh \ + configure_container_engine.sh \ + install.sh \ + uninstall.sh \ + install_init_units.sh \ + install_container.sh \ + start_edgelet.sh \ + wait_edgelet_ready.sh \ + bundled.sh +do + _publish "$SCRIPT_DIR/$_script" "$_script" +done + +for _lib in "$SCRIPT_DIR"/lib/*.sh; do + [ -f "$_lib" ] || continue + _publish "$_lib" "lib/$(basename "$_lib")" +done + +info "Bundled edgelet scripts at ${_share}/" diff --git a/assets/edgelet/scripts/check_prereqs.sh b/assets/edgelet/scripts/check_prereqs.sh new file mode 100755 index 000000000..06f5aae0f --- /dev/null +++ b/assets/edgelet/scripts/check_prereqs.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Remote: passwordless sudo required. Local: set LOCAL_INSTALL=1 to skip sudo probe. +set -e +set -x + +if [ "${LOCAL_INSTALL:-0}" = "1" ]; then + echo "# Local install: skipping passwordless sudo check" + exit 0 +fi + +if ! sudo ls /tmp/ > /dev/null 2>&1; then + MSG="Unable to successfully use sudo with user $USER on this host.\nUser $USER must be in sudoers group and using sudo without password must be enabled." + echo "$MSG" + exit 1 +fi diff --git a/assets/edgelet/scripts/configure_container_edgelet.sh b/assets/edgelet/scripts/configure_container_edgelet.sh new file mode 100644 index 000000000..e286b521d --- /dev/null +++ b/assets/edgelet/scripts/configure_container_edgelet.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# Apply agent spec to a running edgelet container on desktop hosts, then restart the container. +set -e +set -x + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/lib/common.sh" +. "$SCRIPT_DIR/lib/container_mounts.sh" + +EDGELET_DETECT_SOURCED=1 +. "$SCRIPT_DIR/detect_init.sh" +init + +if [ "${EDGELET_INSTALL_MODE:-native}" != "container" ]; then + exit 0 +fi +if ! is_desktop_container_host; then + exit 0 +fi + +wait_edgelet_api_running() { + _iter=0 + _max="${EDGELET_CONFIG_TIMEOUT:-120}" + while [ "$_iter" -lt "$_max" ]; do + _out="" + _out=$(edgelet system status 2>&1) || true + if echo "$_out" | grep -qi 'daemon is not running'; then + sleep 1 + _iter=$((_iter + 1)) + continue + fi + if echo "$_out" | grep -q 'iofogDaemon: RUNNING'; then + return 0 + fi + if echo "$_out" | grep -q 'runtime.agentPhase: running'; then + return 0 + fi + sleep 1 + _iter=$((_iter + 1)) + done + die "Timed out after ${_max}s waiting for edgelet API before configure" +} + +wait_edgelet_api_running + +if [ -n "${EDGELET_BOOTSTRAP_CONFIG_CMD:-}" ]; then + info "Applying edgelet bootstrap configuration" + sh -c "$EDGELET_BOOTSTRAP_CONFIG_CMD" +else + info "No EDGELET_BOOTSTRAP_CONFIG_CMD set; skipping edgelet config" +fi + +restart_edgelet_container +info "Edgelet container configured and restarted" diff --git a/assets/edgelet/scripts/configure_container_engine.sh b/assets/edgelet/scripts/configure_container_engine.sh new file mode 100755 index 000000000..4f233d04b --- /dev/null +++ b/assets/edgelet/scripts/configure_container_engine.sh @@ -0,0 +1,111 @@ +#!/bin/sh +# Configure an existing docker/podman engine. Does not install packages. +set -e +set -x + +if [ "${EDGELET_OS:-}" = "darwin" ] || [ "${EDGELET_OS:-}" = "windows" ]; then + echo "# Skipping configure_container_engine on ${EDGELET_OS} (desktop container runtime is user-managed)" + exit 0 +fi + +CONTAINER_ENGINE="${CONTAINER_ENGINE:-docker}" + +check_docker_version() { + docker_version_num=0 + if command_exists docker; then + raw=$(docker -v 2>/dev/null | sed 's/.*version \([^,]*\),.*/\1/' | tr -d '.') + [ -n "$raw" ] && docker_version_num="$raw" + fi + [ "$docker_version_num" -ge 2610 ] 2>/dev/null +} + +check_podman_version() { + podman_version_num=0 + if command_exists podman; then + raw=$(podman --version 2>/dev/null | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p') + [ -n "$raw" ] && podman_version_num="$raw" + fi + [ "$podman_version_num" -ge 3 ] 2>/dev/null +} + +command_exists() { + command -v "$@" > /dev/null 2>&1 +} + +start_docker() { + if command_exists docker && docker ps >/dev/null 2>&1; then + return 0 + fi + case "${INIT_SYSTEM:-unknown}" in + systemd) sudo systemctl start docker ;; + *) sudo service docker start 2>/dev/null || sudo systemctl start docker ;; + esac +} + +start_podman() { + case "${INIT_SYSTEM:-unknown}" in + systemd) + sudo systemctl start podman 2>/dev/null || true + sudo systemctl start podman.socket 2>/dev/null || true + ;; + *) sudo service podman start 2>/dev/null || true ;; + esac +} + +configure_docker() { + if ! command_exists docker; then + echo "Error: docker is not installed. Install Docker 26.10+ manually or provide scripts.deps." + exit 1 + fi + if ! check_docker_version; then + echo "Error: Docker 26.10+ is required." + exit 1 + fi + start_docker + if [ ! -f /etc/docker/daemon.json ]; then + sudo mkdir -p /etc/docker + sudo tee /etc/docker/daemon.json >/dev/null <<'EOF' +{ + "storage-driver": "overlayfs", + "features": { + "containerd-snapshotter": true, + "cdi": true + }, + "cdi-spec-dirs": ["/etc/cdi/", "/var/run/cdi"] +} +EOF + else + echo "# /etc/docker/daemon.json already exists" + fi +} + +configure_podman() { + if ! command_exists podman; then + echo "Error: podman is not installed. Install Podman 3.0+ manually or provide scripts.deps." + exit 1 + fi + if ! check_podman_version; then + echo "Error: Podman 3.0+ is required." + exit 1 + fi + sudo mkdir -p /etc/cdi /var/run/cdi /etc/containers + if [ ! -f /etc/containers/containers.conf ]; then + sudo tee /etc/containers/containers.conf >/dev/null <<'EOF' +[engine] +runtime = "crun" +cdi_spec_dirs = ["/etc/cdi", "/var/run/cdi"] +EOF + fi + start_podman +} + +case "$CONTAINER_ENGINE" in + docker) configure_docker ;; + podman) configure_podman ;; + *) + echo "Error: unsupported container engine for configure step: $CONTAINER_ENGINE" + exit 1 + ;; +esac + +echo "# Container engine $CONTAINER_ENGINE configured" diff --git a/assets/edgelet/scripts/detect_init.sh b/assets/edgelet/scripts/detect_init.sh new file mode 100755 index 000000000..58bc3c630 --- /dev/null +++ b/assets/edgelet/scripts/detect_init.sh @@ -0,0 +1,101 @@ +#!/bin/sh +# Detect host OS, arch, and init system for edgelet install layers. +set -e + +command_exists() { + command -v "$@" > /dev/null 2>&1 +} + +detect_os() { + case "$(uname -s)" in + Linux) EDGELET_OS=linux ;; + Darwin) EDGELET_OS=darwin ;; + CYGWIN*|MINGW*|MSYS*) EDGELET_OS=windows ;; + *) + echo "Error: unsupported operating system: $(uname -s)" + exit 1 + ;; + esac + export EDGELET_OS +} + +detect_arch() { + if [ -n "${EDGELET_ARCH:-}" ] && [ "$EDGELET_ARCH" != "auto" ]; then + export EDGELET_ARCH + return + fi + + machine="$(uname -m)" + case "$machine" in + x86_64|amd64) EDGELET_ARCH=amd64 ;; + aarch64|arm64) EDGELET_ARCH=arm64 ;; + armv7l|armv6l|arm) EDGELET_ARCH=arm ;; + riscv64) EDGELET_ARCH=riscv64 ;; + *) + echo "Error: unsupported architecture: $machine" + exit 1 + ;; + esac + export EDGELET_ARCH +} + +openrc_is_pid1() { + [ -x /sbin/openrc-run ] || return 1 + if rc-status -s >/dev/null 2>&1; then + return 0 + fi + if [ -f /etc/inittab ] && grep -q '/sbin/openrc' /etc/inittab 2>/dev/null; then + return 0 + fi + _init="$(readlink -f /sbin/init 2>/dev/null || readlink /sbin/init 2>/dev/null || true)" + case "${_init}" in + *openrc*) return 0 ;; + esac + return 1 +} + +procd_is_openwrt() { + [ -x /sbin/procd ] && [ -f /etc/rc.common ] || return 1 + return 0 +} + +detect_init_system() { + INIT_SYSTEM=unknown + if [ "$EDGELET_OS" = "linux" ]; then + if command_exists systemctl && [ -d /etc/systemd/system ]; then + INIT_SYSTEM=systemd + elif procd_is_openwrt; then + INIT_SYSTEM=procd + elif openrc_is_pid1 && { + command_exists openrc \ + || [ -x /sbin/openrc-run ] \ + || [ -f /sbin/openrc ] + }; then + INIT_SYSTEM=openrc + elif command_exists initctl && [ -d /etc/init ]; then + INIT_SYSTEM=upstart + elif [ -d /etc/s6 ] || command_exists s6-svc; then + INIT_SYSTEM=s6 + elif command_exists runsvdir || [ -d /etc/runit ]; then + INIT_SYSTEM=runit + elif [ -f /etc/inittab ] || command_exists update-rc.d || command_exists chkconfig; then + INIT_SYSTEM=sysvinit + fi + elif [ "$EDGELET_OS" = "darwin" ]; then + INIT_SYSTEM=launchd + elif [ "$EDGELET_OS" = "windows" ]; then + INIT_SYSTEM=windows + fi + export INIT_SYSTEM +} + +init() { + detect_os + detect_arch + detect_init_system + echo "# Detected edgelet host: os=$EDGELET_OS arch=$EDGELET_ARCH init=$INIT_SYSTEM" +} + +if [ "${BASH_SOURCE:-$0}" = "$0" ] && [ -z "${EDGELET_DETECT_SOURCED:-}" ]; then + init +fi diff --git a/assets/edgelet/scripts/install.sh b/assets/edgelet/scripts/install.sh new file mode 100644 index 000000000..a44dc6467 --- /dev/null +++ b/assets/edgelet/scripts/install.sh @@ -0,0 +1,208 @@ +#!/bin/sh +# install.sh — Edgelet installer (potctl chunked fork; upstream parity for upgrade/rollback) +# +# Usage: +# sudo ./install.sh --version=v1.0.0-rc.6 +# sudo ./install.sh --airgap --bin-path=/path/to/edgelet-linux-amd64 --version=v1.0.0-rc.6 +# sudo ./install.sh --upgrade --version=v1.0.0-rc.6 +# sudo ./install.sh --rollback +# +# potctl deploy uses --skip-config and --skip-start (config/start handled by iofogctl/potctl). + +set -e +set -x + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/lib/common.sh" +. "$SCRIPT_DIR/lib/paths.sh" +. "$SCRIPT_DIR/lib/receipt.sh" +. "$SCRIPT_DIR/lib/binary.sh" + +EDGELET_VERSION="${EDGELET_VERSION:-latest}" +CONTAINER_ENGINE="" +ACTION="install" +AIRGAP=false +BIN_PATH="" +FORCE_CONFIG=false +WITH_SAMPLE_CA=false +ARCH_OVERRIDE="" +CHECKSUM_FILE="" +EXPECTED_SHA256="" +SKIP_CONFIG=false +SKIP_START=false + +for arg in "$@"; do + case "${arg}" in + --version=*) EDGELET_VERSION="${arg#*=}" ;; + --arch=*) ARCH_OVERRIDE="${arg#*=}" ;; + --container-engine=*) CONTAINER_ENGINE="${arg#*=}" ;; + --bin-path=*) BIN_PATH="${arg#*=}" ;; + --checksum-path=*) CHECKSUM_FILE="${arg#*=}" ;; + --expected-sha256=*) EXPECTED_SHA256="${arg#*=}" ;; + --airgap) AIRGAP=true ;; + --upgrade) ACTION="upgrade" ;; + --rollback) ACTION="rollback" ;; + --force-config) FORCE_CONFIG=true ;; + --with-sample-ca) WITH_SAMPLE_CA=true ;; + --skip-config) SKIP_CONFIG=true ;; + --skip-start) SKIP_START=true ;; + --help|-h) + echo "Usage: $0 [--version=VERSION] [--arch=ARCH] [--container-engine=ENGINE]" + echo " [--airgap] [--bin-path=PATH] [--upgrade] [--rollback]" + echo " [--skip-config] [--skip-start] # potctl internal" + exit 0 + ;; + *) die "Unknown option: ${arg}" ;; + esac +done + +EDGELET_DETECT_SOURCED=1 +. "$SCRIPT_DIR/detect_init.sh" +init + +OS="$EDGELET_OS" +ARCH="${ARCH_OVERRIDE:-$EDGELET_ARCH}" +if [ -z "$CONTAINER_ENGINE" ]; then + CONTAINER_ENGINE="${CONTAINER_ENGINE:-$(default_container_engine_for_os "$OS")}" +fi +case "$CONTAINER_ENGINE" in + edgelet) + [ "$OS" = "linux" ] || die "containerEngine=edgelet is linux-only" + ;; + docker|podman) ;; + *) die "Invalid --container-engine (use edgelet, docker, or podman)" ;; +esac + +if [ "$OS" != "windows" ]; then + [ "$(id -u)" -eq 0 ] || die "Must be run as root. Try: sudo $0 $*" +fi + +init_platform_paths "$OS" +BINARY_PATH="$(binary_path_for_os "$OS")" +INIT="${INIT_SYSTEM:-unknown}" + +info "OS: ${OS} Arch: ${ARCH} Init: ${INIT} Engine: ${CONTAINER_ENGINE} Action: ${ACTION}" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "${TMPDIR}"' EXIT + +if [ "$AIRGAP" = false ] && [ "$EDGELET_VERSION" = "latest" ] && [ "$ACTION" != "rollback" ] && [ -z "$BIN_PATH" ]; then + EDGELET_VERSION=$(fetch_latest_version) +fi +info "Version: ${EDGELET_VERSION}" + +_run_post_install() { + if [ "$SKIP_START" = true ]; then + info "Skipping daemon start (--skip-start); use start_edgelet.sh" + return 0 + fi + if [ "$OS" = "linux" ]; then + EDGELET_INSTALL_MODE=native CONTAINER_ENGINE="$CONTAINER_ENGINE" \ + "$SCRIPT_DIR/install_init_units.sh" || true + EDGELET_INSTALL_MODE=native CONTAINER_ENGINE="$CONTAINER_ENGINE" \ + "$SCRIPT_DIR/start_edgelet.sh" + elif [ "$OS" = "darwin" ]; then + "$SCRIPT_DIR/start_edgelet.sh" + fi +} + +if [ "$ACTION" = "rollback" ]; then + [ -f "$PREVIOUS_FILE" ] || die "No ${PREVIOUS_FILE} found." + _pv=$(kv_get "$PREVIOUS_FILE" "previous_version") + _pos=$(kv_get "$PREVIOUS_FILE" "previous_os") + _parch=$(kv_get "$PREVIOUS_FILE" "previous_arch") + _peng=$(kv_get "$PREVIOUS_FILE" "previous_container_engine") + _purl=$(kv_get "$PREVIOUS_FILE" "previous_download_url") + _cfgbak=$(kv_get "$PREVIOUS_FILE" "config_backup_path") + [ -n "$_pos" ] || _pos="$OS" + [ -n "$_parch" ] || _parch="$ARCH" + CONTAINER_ENGINE="${_peng:-edgelet}" + EDGELET_VERSION="$_pv" + _staged="${TMPDIR}/edgelet-bin" + _cached=$(cached_binary_path "$_pv" "$_pos" "$_parch") + if [ -n "$_cached" ]; then + cp "$_cached" "$_staged" + elif [ -n "$BIN_PATH" ]; then + verify_binary_checksum "$BIN_PATH" + cp "$BIN_PATH" "$_staged" + elif [ "$AIRGAP" = true ]; then + die "rollback with --airgap requires --bin-path or a cached binary" + else + curl -fsSL -o "$_staged" "$_purl" || die "Failed to download rollback binary" + fi + install_binary_file "$_staged" "$BINARY_PATH" + install_dirs_for_os "$OS" + if [ "$FORCE_CONFIG" != true ] && [ -f "$_cfgbak" ]; then + maybe_sudo install -m 640 "$_cfgbak" "$CONFIG_FILE" + fi + _sha=$(sha256_file "$BINARY_PATH") + write_install_receipt "$EDGELET_VERSION" "$_pos" "$_parch" "$CONTAINER_ENGINE" "$_purl" "$_sha" "rollback" + _run_post_install + "$SCRIPT_DIR/bundled.sh" || true + info "Rollback to ${EDGELET_VERSION} complete." + exit 0 +fi + +if [ "$ACTION" = "upgrade" ]; then + [ -f "$BINARY_PATH" ] || die "Edgelet not installed; run install first" + [ -f "$RECEIPT_FILE" ] || die "Missing ${RECEIPT_FILE}" + _cur_ver=$(kv_get "$RECEIPT_FILE" "installed_version") + _cur_os=$(kv_get "$RECEIPT_FILE" "os") + _cur_arch=$(kv_get "$RECEIPT_FILE" "arch") + _cur_eng=$(kv_get "$RECEIPT_FILE" "container_engine") + _cur_src=$(kv_get "$RECEIPT_FILE" "source_url") + _cur_sha=$(kv_get "$RECEIPT_FILE" "binary_sha256") + [ -n "$_cur_os" ] || _cur_os="$OS" + [ -n "$_cur_arch" ] || _cur_arch="$ARCH" + [ -n "$_cur_eng" ] || _cur_eng="$CONTAINER_ENGINE" + if [ "$EDGELET_VERSION" = "latest" ] && [ -z "$BIN_PATH" ]; then + EDGELET_VERSION=$(fetch_latest_version) + fi + _cfg_backup="${BACKUP_DIR}/config.yaml.$(date +%Y%m%d%H%M%S 2>/dev/null || date +%s)" + cp "$CONFIG_FILE" "$_cfg_backup" 2>/dev/null || true + cache_binary "$_cur_ver" "$_cur_os" "$_cur_arch" "$BINARY_PATH" + write_previous_release "$_cur_ver" "$_cur_os" "$_cur_arch" "$_cur_eng" "$_cur_src" "$_cur_sha" "$_cfg_backup" + stop_edgelet_daemon_desktop "$OS" + _staged="${TMPDIR}/edgelet-bin" + download_or_stage_binary "$_staged" + verify_binary_checksum "$_staged" + install_binary_file "$_staged" "$BINARY_PATH" + install_dirs_for_os "$OS" + _sha=$(sha256_file "$BINARY_PATH") + _method="upgrade" + [ "$AIRGAP" = true ] && _method="upgrade-airgap" + write_install_receipt "$EDGELET_VERSION" "$OS" "$ARCH" "$CONTAINER_ENGINE" "$(compute_source_url)" "$_sha" "$_method" + "$SCRIPT_DIR/bundled.sh" || true + _run_post_install + info "Upgrade to ${EDGELET_VERSION} complete." + exit 0 +fi + +# fresh install +if command -v edgelet >/dev/null 2>&1; then + installed=$(edgelet --version 2>/dev/null | head -n1 | tr -d '[:space:]') + if [ -n "$EDGELET_VERSION" ] && [ "$installed" = "$EDGELET_VERSION" ]; then + info "Edgelet $EDGELET_VERSION already installed." + "$SCRIPT_DIR/bundled.sh" || true + exit 0 + fi +fi + +_staged="${TMPDIR}/edgelet-bin" +download_or_stage_binary "$_staged" +verify_binary_checksum "$_staged" +install_dirs_for_os "$OS" +install_binary_file "$_staged" "$BINARY_PATH" +_sha=$(sha256_file "$BINARY_PATH") +_method="install" +[ "$AIRGAP" = true ] && _method="install-airgap" +write_install_receipt "$EDGELET_VERSION" "$OS" "$ARCH" "$CONTAINER_ENGINE" "$(compute_source_url)" "$_sha" "$_method" +"$SCRIPT_DIR/bundled.sh" || true + +if [ "$SKIP_START" = true ]; then + info "Skipping daemon start (--skip-start); potctl will start after config materialization." + exit 0 +fi + +_run_post_install +info "edgelet ${EDGELET_VERSION} installed (os=${OS} engine=${CONTAINER_ENGINE})." diff --git a/assets/edgelet/scripts/install_container.sh b/assets/edgelet/scripts/install_container.sh new file mode 100644 index 000000000..6d60c7a36 --- /dev/null +++ b/assets/edgelet/scripts/install_container.sh @@ -0,0 +1,75 @@ +#!/bin/sh +# Prepare containerized edgelet deployment (deploymentType=container). +set -e +set -x + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/lib/common.sh" +. "$SCRIPT_DIR/lib/paths.sh" +. "$SCRIPT_DIR/lib/receipt.sh" +. "$SCRIPT_DIR/lib/container_cli.sh" +. "$SCRIPT_DIR/lib/container_mounts.sh" + +IMAGE="" +ENGINE="" +TZ="${EDGELET_TZ:-UTC}" + +for arg in "$@"; do + case "${arg}" in + --image=*) IMAGE="${arg#*=}" ;; + --engine=*) ENGINE="${arg#*=}" ;; + --tz=*) TZ="${arg#*=}" ;; + --help|-h) + echo "Usage: $0 --image=IMAGE [--engine=docker|podman] [--tz=TZ]" + exit 0 + ;; + *) die "Unknown option: ${arg}" ;; + esac +done + +[ -n "$IMAGE" ] || die "--image is required" + +EDGELET_DETECT_SOURCED=1 +. "$SCRIPT_DIR/detect_init.sh" +init +init_platform_paths "$EDGELET_OS" + +EDGELET_CONTAINER_NAME="${EDGELET_CONTAINER_NAME:-edgelet}" + +export EDGELET_CONTAINER_IMAGE="$IMAGE" +export EDGELET_TZ="$TZ" +export CONTAINER_ENGINE="$ENGINE" +export EDGELET_CONTAINER_NAME +export EDGELET_INSTALL_MODE=container + +if [ -z "$ENGINE" ]; then + ENGINE="docker" + export CONTAINER_ENGINE="$ENGINE" +fi + +if ! is_desktop_container_host; then + install_dirs_for_os "$EDGELET_OS" +fi + +_runtime="" +case "$ENGINE" in + docker) _runtime="docker" ;; + podman) _runtime="podman" ;; + *) die "container deployment requires docker or podman engine (got $ENGINE)" ;; +esac + +if ! command -v "$_runtime" >/dev/null 2>&1; then + die "$_runtime is not installed" +fi + +info "Pulling container image $IMAGE" +maybe_sudo "$_runtime" pull "$IMAGE" + +install_container_cli_wrapper "$ENGINE" "$EDGELET_CONTAINER_NAME" "$EDGELET_OS" + +if desktop_container_local; then + info "Skipping bundled publish on desktop local container deploy" +else + "$SCRIPT_DIR/bundled.sh" || true +fi +info "Container edgelet prepared (image=$IMAGE engine=$ENGINE tz=$TZ)" diff --git a/assets/edgelet/scripts/install_deps.sh b/assets/edgelet/scripts/install_deps.sh new file mode 100755 index 000000000..f98a5e406 --- /dev/null +++ b/assets/edgelet/scripts/install_deps.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# Install deps layer. Skips when containerEngine=edgelet. +set -e +set -x + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +CONTAINER_ENGINE="${1:-edgelet}" +DEPLOYMENT_TYPE="${2:-native}" + +if [ "$CONTAINER_ENGINE" = "edgelet" ]; then + echo "# Skipping install_deps: containerEngine=edgelet (deploymentType=$DEPLOYMENT_TYPE)" + exit 0 +fi + +export CONTAINER_ENGINE +EDGELET_DETECT_SOURCED=1 +. "$SCRIPT_DIR/detect_init.sh" +init + +if [ "$EDGELET_OS" = "darwin" ] || [ "$EDGELET_OS" = "windows" ]; then + echo "# Skipping configure_container_engine on ${EDGELET_OS} (desktop container runtime is user-managed)" + exit 0 +fi + +. "$SCRIPT_DIR/configure_container_engine.sh" diff --git a/assets/edgelet/scripts/install_init_units.sh b/assets/edgelet/scripts/install_init_units.sh new file mode 100755 index 000000000..79613f9de --- /dev/null +++ b/assets/edgelet/scripts/install_init_units.sh @@ -0,0 +1,760 @@ +#!/bin/sh +# Install edgelet init units, engine drop-ins, and container deployment host units. +set -e +set -x + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +EDGELET_DETECT_SOURCED=1 +. "$SCRIPT_DIR/detect_init.sh" +init + +CONTAINER_ENGINE="${CONTAINER_ENGINE:-edgelet}" +DEPLOYMENT_TYPE="${DEPLOYMENT_TYPE:-native}" +EDGELET_INSTALL_MODE="${EDGELET_INSTALL_MODE:-native}" +EDGELET_CONTAINER_IMAGE="${EDGELET_CONTAINER_IMAGE:-}" +EDGELET_TZ="${EDGELET_TZ:-UTC}" +EDGELET_LIBEXEC="/usr/libexec/edgelet" +EDGELET_CONTAINER_NAME="${EDGELET_CONTAINER_NAME:-edgelet}" + +. "$SCRIPT_DIR/lib/common.sh" +. "$SCRIPT_DIR/lib/container_engine.sh" +. "$SCRIPT_DIR/lib/container_mounts.sh" + +install_shutdown_helper() { + mkdir -p "${EDGELET_LIBEXEC}" + cat > "/usr/libexec/edgelet/edgelet-shutdown" << 'EDGELET_SHUTDOWN_EOF' +#!/bin/sh +# edgelet-shutdown — shared control-plane stop entry for all init systems (Plan 10). +# Plan 11: v1 default skips MS drain on control stop (shutdownPolicy=leave-running for docker/podman; +# embedded split uses attach-only). shutdownGracePeriodSeconds applies to optional maintenance drain +# and data-plane (edgelet-containerd) stop — not control-plane MS drain by default. +set -e +EDGELET="${EDGELET_BIN:-/usr/local/bin/edgelet}" +exec "${EDGELET}" shutdown "$@" +EDGELET_SHUTDOWN_EOF + chmod 755 "/usr/libexec/edgelet/edgelet-shutdown" +} +write_systemd_edgelet_service() { + cat > "/etc/systemd/system/edgelet.service" << 'EDGELET_SYSTEMD_EDGELET_SERVICE_EOF' +[Unit] +Description=Edgelet daemon (control plane) +Documentation=https://github.com/eclipse-iofog/edgelet +Wants=network-online.target +After=network-online.target +StartLimitIntervalSec=300 +StartLimitBurst=20 + +[Service] +Type=simple +ExecStartPre=/bin/sh -c 'mountpoint -q /sys/fs/bpf || mount -t bpf bpf /sys/fs/bpf 2>/dev/null || true' +ExecStart=/usr/local/bin/edgelet daemon +ExecStop=/usr/libexec/edgelet/edgelet-shutdown +Restart=always +RestartSec=2s +# Default 120s = shutdownGracePeriodSeconds (90) + 30s buffer; edgelet-engine drop-in may override. +TimeoutStopSec=120s +KillMode=process +Delegate=yes +DelegateSubgroup=supervisor +KillSignal=SIGTERM +SendSIGKILL=yes +User=root +StandardOutput=journal +StandardError=journal +SyslogIdentifier=edgelet + +# Embedded engine needs host paths under /etc/cni, /run, /var, /opt (monolithic unit). +# Tighten on edgelet-containerd.service after Plan 11 data-plane split. +NoNewPrivileges=no + +# Resource limits +LimitNOFILE=65536 +LimitNPROC=4096 + +[Install] +WantedBy=multi-user.target +EDGELET_SYSTEMD_EDGELET_SERVICE_EOF + chmod 644 "/etc/systemd/system/edgelet.service" +} +write_systemd_edgelet_containerd_service() { + cat > "/etc/systemd/system/edgelet-containerd.service" << 'EDGELET_SYSTEMD_EDGELET_CONTAINERD_SERVICE_EOF' +# Edgelet embedded containerd (data plane) — Plan 11 workload continuity +[Unit] +Description=Edgelet embedded containerd (data plane) +Documentation=https://github.com/eclipse-iofog/edgelet +Before=edgelet.service +After=network-online.target +Wants=network-online.target +# Intentionally NOT PartOf=edgelet.service: control restart/stop must not +# stop the data plane (Plan 11 attach-only). Full teardown: stop both units. + +[Service] +Type=simple +ExecStartPre=/bin/sh -c 'mountpoint -q /sys/fs/bpf || mount -t bpf bpf /sys/fs/bpf 2>/dev/null || true' +ExecStart=/usr/local/bin/edgelet runtime-bootstrap +Restart=always +RestartSec=2s +# Data-plane stop: drain MS via runtime-bootstrap SIGTERM handler + shutdownGracePeriodSeconds +TimeoutStopSec=120s +KillMode=process +KillSignal=SIGTERM +SendSIGKILL=yes +User=root +StandardOutput=journal +StandardError=journal +SyslogIdentifier=edgelet-containerd +Delegate=yes +DelegateSubgroup=containerd +NoNewPrivileges=no +LimitNOFILE=65536 +LimitNPROC=4096 + +[Install] +WantedBy=multi-user.target +EDGELET_SYSTEMD_EDGELET_CONTAINERD_SERVICE_EOF + chmod 644 "/etc/systemd/system/edgelet-containerd.service" +} +write_systemd_dropin_docker() { + cat > "/etc/systemd/system/edgelet.service.d/docker.conf" << 'EDGELET_SYSTEMD_DROPIN_DOCKER_EOF' +# Engine ordering drop-in — docker (Plan 10). Installed when containerEngine=docker. +[Unit] +After=network-online.target docker.service +Wants=docker.service +EDGELET_SYSTEMD_DROPIN_DOCKER_EOF + chmod 644 "/etc/systemd/system/edgelet.service.d/docker.conf" +} +write_systemd_dropin_podman() { + cat > "/etc/systemd/system/edgelet.service.d/podman.conf" << 'EDGELET_SYSTEMD_DROPIN_PODMAN_EOF' +# Engine ordering drop-in — podman (Plan 10). Installed when containerEngine=podman. +[Unit] +After=network-online.target podman.socket +Wants=podman.socket +EDGELET_SYSTEMD_DROPIN_PODMAN_EOF + chmod 644 "/etc/systemd/system/edgelet.service.d/podman.conf" +} +write_systemd_dropin_edgelet() { + cat > "/etc/systemd/system/edgelet.service.d/edgelet.conf" << 'EDGELET_SYSTEMD_DROPIN_EDGELET_EOF' +# Engine ordering drop-in — embedded edgelet (Plan 11). Installed when containerEngine=edgelet. +[Unit] +Wants=edgelet-containerd.service +After=network-online.target edgelet-containerd.service + +[Service] +Environment=EDGELET_RUNTIME_SPLIT=1 +# Control-plane stop: default leave-running (skip MS drain). Grace + buffer matches shutdownGracePeriodSeconds default 90. +TimeoutStopSec=120s +EDGELET_SYSTEMD_DROPIN_EDGELET_EOF + chmod 644 "/etc/systemd/system/edgelet.service.d/edgelet.conf" +} +write_openrc_edgelet_init() { + cat > "/etc/init.d/edgelet" << 'EDGELET_OPENRC_EDGELET_INIT_EOF' +#!/sbin/openrc-run + +name="edgelet" +description="Edgelet daemon (control plane)" +command="/usr/local/bin/edgelet" +command_args="daemon" +supervisor="supervise-daemon" +respawn_delay=2 +respawn_max=0 +respawn_period=3600 +command_user="root" +pidfile="/run/${RC_SVCNAME}.pid" +output_log="/var/log/edgelet/daemon.log" +error_log="/var/log/edgelet/daemon.log" +shutdown="/usr/libexec/edgelet/edgelet-shutdown" + +depend() { + need net + after firewall +%%EDGELET_ENGINE_NEED%% +} + +wait_containerd_attach_socket() { + _timeout="${EDGELET_ATTACH_WAIT_SEC:-120}" + _elapsed=0 + _sock="/run/edgelet/containerd.sock" + while [ "${_elapsed}" -lt "${_timeout}" ]; do + if [ -S "${_sock}" ] || [ -S /var/run/edgelet/containerd.sock ]; then + [ -S "${_sock}" ] || _sock="/var/run/edgelet/containerd.sock" + _ctr="" + if [ -x /var/lib/edgelet/data/current/bin/ctr ]; then + _ctr=/var/lib/edgelet/data/current/bin/ctr + elif command -v ctr >/dev/null 2>&1; then + _ctr=ctr + fi + if [ -z "${_ctr}" ] || "${_ctr}" --address "${_sock}" version >/dev/null 2>&1; then + return 0 + fi + fi + sleep 2 + _elapsed=$(( _elapsed + 2 )) + done + ewarn "data-plane containerd socket not ready after ${_timeout}s" + return 1 +} + +start_pre() { + /usr/local/bin/edgelet cgroup-preflight || return $? + if [ -f /etc/init.d/edgelet-containerd ]; then + export EDGELET_RUNTIME_SPLIT=1 + wait_containerd_attach_socket || return $? + fi +} + +stop_pre() { + if [ -f /etc/init.d/edgelet-containerd ]; then + export EDGELET_RUNTIME_SPLIT=1 + fi +} + +stop() { + ebegin "Stopping ${RC_SVCNAME}" + if ! "${shutdown}"; then + # Fallback when EdgeletAPI is down: single TERM/KILL, no SSD --retry (hangs after graceful stop). + if [ -f "${pidfile}" ]; then + _pid=$(cat "${pidfile}" 2>/dev/null || true) + if [ -n "${_pid}" ] && kill -0 "${_pid}" 2>/dev/null; then + kill -TERM "${_pid}" 2>/dev/null || true + sleep 2 + kill -KILL "${_pid}" 2>/dev/null || true + fi + fi + rm -f "${pidfile}" /run/edgelet/edgelet.pid 2>/dev/null || true + eend 1 + return 1 + fi + rm -f "${pidfile}" /run/edgelet/edgelet.pid 2>/dev/null || true + # Plan 11 split: data-plane containerd is edgelet-containerd service — do not reap child here. + if [ ! -f /etc/init.d/edgelet-containerd ]; then + _pids=$(pgrep -f edgelet-containerd-child 2>/dev/null || true) + for _p in ${_pids}; do kill -TERM "${_p}" 2>/dev/null || true; done + sleep 2 + _pids=$(pgrep -f edgelet-containerd-child 2>/dev/null || true) + for _p in ${_pids}; do kill -KILL "${_p}" 2>/dev/null || true; done + fi + eend 0 +} +EDGELET_OPENRC_EDGELET_INIT_EOF + chmod 755 "/etc/init.d/edgelet" +} +write_openrc_edgelet_containerd_init() { + cat > "/etc/init.d/edgelet-containerd" << 'EDGELET_OPENRC_EDGELET_CONTAINERD_INIT_EOF' +#!/sbin/openrc-run + +# Edgelet embedded containerd (data plane) — Plan 11 workload continuity +name="edgelet-containerd" +description="Edgelet embedded containerd (data plane)" +command="/usr/local/bin/edgelet" +command_args="runtime-bootstrap" +command_background=true +command_user="root" +pidfile="/run/${RC_SVCNAME}.pid" +output_log="/var/log/edgelet/containerd.log" +error_log="/var/log/edgelet/containerd.log" + +depend() { + need net + need edgelet-cgroup-prep + before edgelet +} + +start_pre() { + # C3: light preflight only; primary cgroup bootstrap is in runtime-bootstrap (C1). + mountpoint -q /sys/fs/bpf 2>/dev/null || mount -t bpf bpf /sys/fs/bpf 2>/dev/null || true + /usr/local/bin/edgelet cgroup-preflight || return $? +} + +start_post() { + # Block dependency satisfaction until CRI socket answers (Plan 11 split). + _timeout="${EDGELET_CONTAINERD_READY_SEC:-120}" + _elapsed=0 + _sock="/run/edgelet/containerd.sock" + while [ "${_elapsed}" -lt "${_timeout}" ]; do + if [ -S "${_sock}" ] || [ -S /var/run/edgelet/containerd.sock ]; then + [ -S "${_sock}" ] || _sock="/var/run/edgelet/containerd.sock" + _ctr="" + if [ -x /var/lib/edgelet/data/current/bin/ctr ]; then + _ctr=/var/lib/edgelet/data/current/bin/ctr + elif command -v ctr >/dev/null 2>&1; then + _ctr=ctr + fi + if [ -z "${_ctr}" ] || "${_ctr}" --address "${_sock}" version >/dev/null 2>&1; then + return 0 + fi + fi + sleep 2 + _elapsed=$(( _elapsed + 2 )) + done + ewarn "containerd socket not ready after ${_timeout}s" + return 1 +} + +stop() { + ebegin "Stopping ${RC_SVCNAME}" + if [ -f "${pidfile}" ]; then + _pid=$(cat "${pidfile}" 2>/dev/null || true) + if [ -n "${_pid}" ] && kill -0 "${_pid}" 2>/dev/null; then + kill -TERM "${_pid}" 2>/dev/null || true + _grace="${EDGELET_CONTAINERD_STOP_SEC:-30}" + _elapsed=0 + while kill -0 "${_pid}" 2>/dev/null; do + if [ "${_elapsed}" -ge "${_grace}" ]; then + kill -KILL "${_pid}" 2>/dev/null || true + break + fi + sleep 2 + _elapsed=$(( _elapsed + 2 )) + done + fi + fi + rm -f "${pidfile}" 2>/dev/null || true + eend 0 +} +EDGELET_OPENRC_EDGELET_CONTAINERD_INIT_EOF + chmod 755 "/etc/init.d/edgelet-containerd" +} +write_openrc_edgelet_cgroup_prep_init() { + cat > "/etc/init.d/edgelet-cgroup-prep" << 'EDGELET_OPENRC_EDGELET_CGROUP_PREP_INIT_EOF' +#!/sbin/openrc-run + +# Early cgroup v2 delegation for LXC/VM machine roots (OrbStack Alpine, etc.). +# Moby/dind-style reparent before enabling subtree_control on root and /.lxc. +name="edgelet-cgroup-prep" +description="Edgelet cgroup delegation prep (machine root)" + +depend() { + before edgelet-containerd edgelet +} + +cgroup_delegate_controllers() { + _dir="$1" + _ctrl="${_dir}/cgroup.controllers" + _sub="${_dir}/cgroup.subtree_control" + [ -f "${_ctrl}" ] || return 0 + for _c in cpu memory pids; do + grep -qw "${_c}" "${_ctrl}" 2>/dev/null || continue + grep -qw "${_c}" "${_sub}" 2>/dev/null && continue + echo "+${_c}" >> "${_sub}" 2>/dev/null || true + done +} + +cgroup_reparent_procs() { + _from="$1" + _to="$2" + [ -f "${_from}/cgroup.procs" ] || return 0 + [ -s "${_from}/cgroup.procs" ] || return 0 + mkdir -p "${_to}" + xargs -rn1 < "${_from}/cgroup.procs" > "${_to}/cgroup.procs" 2>/dev/null || true +} + +start() { + # No-op on bare-metal / systemd VM layouts without LXC machine cgroup. + [ -d /sys/fs/cgroup/.lxc ] || return 0 + + ebegin "Preparing cgroup delegation for machine root" + _cg="/sys/fs/cgroup" + + cgroup_reparent_procs "${_cg}" "${_cg}/init" + cgroup_delegate_controllers "${_cg}" + + cgroup_reparent_procs "${_cg}/.lxc" "${_cg}/.lxc/init" + cgroup_delegate_controllers "${_cg}/.lxc" + + eend 0 +} + +stop() { + return 0 +} +EDGELET_OPENRC_EDGELET_CGROUP_PREP_INIT_EOF + chmod 755 "/etc/init.d/edgelet-cgroup-prep" +} +write_procd_edgelet() { + cat > "/etc/init.d/edgelet" << 'EDGELET_PROCD_EDGELET_EOF' +#!/bin/sh /etc/rc.common +# Edgelet control plane (OpenWrt procd) + +START=99 +STOP=10 +USE_PROCD=1 + +start_service() { + /usr/local/bin/edgelet cgroup-preflight || return $? + + procd_open_instance + procd_set_param command /usr/local/bin/edgelet + procd_append_param command daemon + procd_set_param respawn 3600 5 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +stop_service() { + /usr/libexec/edgelet/edgelet-shutdown +} +EDGELET_PROCD_EDGELET_EOF + chmod 755 "/etc/init.d/edgelet" +} +write_sysvinit_edgelet() { + cat > "/etc/init.d/edgelet" << 'EDGELET_SYSVINIT_EDGELET_EOF' +#!/bin/sh +### BEGIN INIT INFO +# Provides: edgelet +# Required-Start: $network $remote_fs +# Required-Stop: $network $remote_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Edgelet daemon +### END INIT INFO + +DAEMON=/usr/local/bin/edgelet +SHUTDOWN=/usr/libexec/edgelet/edgelet-shutdown +PIDFILE=/var/run/edgelet.pid +LOGFILE=/var/log/edgelet/daemon.log +KILLTIMEOUT=30 + +. /lib/lsb/init-functions + +preflight() { + "${DAEMON}" cgroup-preflight +} + +case "$1" in + start) + log_daemon_msg "Starting edgelet" + preflight || exit $? + start-stop-daemon --start --background --make-pidfile --pidfile "$PIDFILE" \ + --exec "$DAEMON" -- daemon >>"$LOGFILE" 2>&1 + log_end_msg $? + ;; + stop) + log_daemon_msg "Stopping edgelet" + if [ -x "$SHUTDOWN" ]; then + "$SHUTDOWN" && log_end_msg 0 && exit 0 + fi + start-stop-daemon --stop --pidfile "$PIDFILE" --retry TERM/${KILLTIMEOUT}/KILL/5 + log_end_msg $? + ;; + restart|force-reload) + $0 stop + $0 start + ;; + status) + status_of_proc -p "$PIDFILE" "$DAEMON" edgelet + ;; + *) + echo "Usage: $0 {start|stop|restart|status}" + exit 1 + ;; +esac + +exit 0 +EDGELET_SYSVINIT_EDGELET_EOF + chmod 755 "/etc/init.d/edgelet" +} +write_upstart_edgelet() { + cat > "/etc/init/edgelet.conf" << 'EDGELET_UPSTART_EDGELET_EOF' +description "Edgelet daemon (control plane)" +author "Datasance" + +start on runlevel [2345] +stop on runlevel [!2345] + +respawn +respawn limit 20 300 + +pre-start script + /usr/local/bin/edgelet cgroup-preflight || exit $? +end script + +exec /usr/local/bin/edgelet daemon >> /var/log/edgelet/daemon.log 2>&1 + +post-stop script + /usr/libexec/edgelet/edgelet-shutdown || true +end script +EDGELET_UPSTART_EDGELET_EOF + chmod 644 "/etc/init/edgelet.conf" +} +write_s6_run() { + cat > "/etc/s6/edgelet/run" << 'EDGELET_S6_RUN_EOF' +#!/bin/sh +exec 2>&1 +/usr/local/bin/edgelet cgroup-preflight || exit $? +exec /usr/local/bin/edgelet daemon +EDGELET_S6_RUN_EOF + chmod 755 "/etc/s6/edgelet/run" +} +write_s6_finish() { + cat > "/etc/s6/edgelet/finish" << 'EDGELET_S6_FINISH_EOF' +#!/bin/sh +/usr/libexec/edgelet/edgelet-shutdown 2>/dev/null || true +exit "${1:-0}" +EDGELET_S6_FINISH_EOF + chmod 755 "/etc/s6/edgelet/finish" +} +write_runit_run() { + cat > "/etc/runit/edgelet/run" << 'EDGELET_RUNIT_RUN_EOF' +#!/bin/sh +LOG=/var/log/edgelet/daemon.log +exec 2>&1 +/usr/local/bin/edgelet cgroup-preflight >>"$LOG" 2>&1 || exit $? +exec /usr/local/bin/edgelet daemon >>"$LOG" 2>&1 +EDGELET_RUNIT_RUN_EOF + chmod 755 "/etc/runit/edgelet/run" +} +write_runit_finish() { + cat > "/etc/runit/edgelet/finish" << 'EDGELET_RUNIT_FINISH_EOF' +#!/bin/sh +/usr/libexec/edgelet/edgelet-shutdown 2>/dev/null || true +exit "${1:-0}" +EDGELET_RUNIT_FINISH_EOF + chmod 755 "/etc/runit/edgelet/finish" +} + +openrc_engine_need_line() { + case "$1" in + docker) printf '%s\n' ' need docker' ;; + podman) printf '%s\n' ' need podman' ;; + edgelet) printf '%s\n' ' need edgelet-containerd' ;; + *) printf '%s\n' '' ;; + esac +} + +apply_openrc_engine_deps() { + _eng="$1" + _dest="$2" + _need="$(openrc_engine_need_line "${_eng}")" + if [ -f "${_dest}" ]; then + # shellcheck disable=SC2016 + awk -v need="${_need}" ' + /%%EDGELET_ENGINE_NEED%%/ { + if (need != "") print need + next + } + { print } + ' "${_dest}" > "${_dest}.tmp" && mv "${_dest}.tmp" "${_dest}" + fi +} + +install_init_helpers() { + install_shutdown_helper +} + +install_systemd_dropin() { + _eng="$1" + _root="$2" + _dropdir="/etc/systemd/system/edgelet.service.d" + mkdir -p "${_dropdir}" + rm -f "${_dropdir}/docker.conf" "${_dropdir}/podman.conf" "${_dropdir}/edgelet.conf" + if [ -n "${_root}" ]; then + case "${_eng}" in + docker) + install -m 644 "${_root}/systemd/edgelet.service.d/docker.conf" "${_dropdir}/docker.conf" + ;; + podman) + install -m 644 "${_root}/systemd/edgelet.service.d/podman.conf" "${_dropdir}/podman.conf" + ;; + edgelet) + install -m 644 "${_root}/systemd/edgelet.service.d/edgelet.conf" "${_dropdir}/edgelet.conf" + systemctl enable edgelet-containerd 2>/dev/null || true + ;; + esac + else + case "${_eng}" in + docker) write_systemd_dropin_docker ;; + podman) write_systemd_dropin_podman ;; + edgelet) + write_systemd_dropin_edgelet + systemctl enable edgelet-containerd 2>/dev/null || true + ;; + esac + fi +} + +write_container_systemd_unit() { + _engine="$1" + _image="$2" + _tz="$3" + _run_bin="" + case "$_engine" in + docker) _run_bin="/usr/bin/docker" ;; + podman) _run_bin="/usr/bin/podman" ;; + *) echo "Error: container deployment requires docker or podman engine"; exit 1 ;; + esac + _sock_mount=$(container_engine_sock_mount) + _vol_mounts=$(edgelet_container_volume_mounts) + prepare_edgelet_container_host_dirs + maybe_sudo tee /etc/systemd/system/edgelet.service > /dev/null < /dev/null </dev/null || true + ;; +restart) + \$0 stop; \$0 start + ;; +*) echo "Usage: \$0 {start|stop|restart}"; exit 1 ;; +esac +EOF + maybe_sudo chmod +x /etc/init.d/edgelet + maybe_sudo rc-update add edgelet default 2>/dev/null || true + echo "# OpenRC container init edgelet installed (engine=${_engine})" +} + +install_container_init_units() { + _engine="${CONTAINER_ENGINE:-docker}" + _image="${EDGELET_CONTAINER_IMAGE:-}" + _tz="${EDGELET_TZ:-UTC}" + if [ -z "$_image" ]; then + echo "Error: EDGELET_CONTAINER_IMAGE is required for container deployment" + exit 1 + fi + case "$INIT_SYSTEM" in + systemd) write_container_systemd_unit "$_engine" "$_image" "$_tz" ;; + openrc) write_container_openrc_init "$_engine" "$_image" "$_tz" ;; + launchd) + echo "# launchd container unit deferred to start_edgelet.sh on darwin" + ;; + *) + echo "Error: container deployment init unit not supported for init=$INIT_SYSTEM" + exit 1 + ;; + esac +} + +install_native_init_units() { + _init="$INIT_SYSTEM" + _eng="$CONTAINER_ENGINE" + + if [ "$EDGELET_OS" = "darwin" ] || [ "$_init" = "launchd" ]; then + echo "# launchd native install deferred to start_edgelet.sh on darwin" + return 0 + fi + + mkdir -p /var/log/edgelet + install_init_helpers + case "${_init}" in + systemd) + mkdir -p /etc/cni/net.d /run/edgelet /run/containerd + chmod 755 /run/edgelet /run/containerd 2>/dev/null || true + write_systemd_edgelet_service + write_systemd_edgelet_containerd_service + install_systemd_dropin "${_eng}" "" + maybe_sudo systemctl daemon-reload + if [ "${_eng}" = "edgelet" ]; then + maybe_sudo systemctl enable edgelet-containerd 2>/dev/null || true + fi + maybe_sudo systemctl enable edgelet + echo "# systemd unit edgelet.service installed (engine=${_eng})" + ;; + openrc) + write_openrc_edgelet_init + write_openrc_edgelet_cgroup_prep_init + write_openrc_edgelet_containerd_init + maybe_sudo rc-update add edgelet-cgroup-prep sysinit 2>/dev/null || true + apply_openrc_engine_deps "${_eng}" /etc/init.d/edgelet + chmod 755 /etc/init.d/edgelet + if [ "${_eng}" = "edgelet" ]; then + maybe_sudo rc-update add edgelet-containerd default 2>/dev/null || true + fi + maybe_sudo rc-update add edgelet default 2>/dev/null || true + echo "# OpenRC service edgelet installed (engine=${_eng})" + ;; + procd) + write_procd_edgelet + maybe_sudo /etc/init.d/edgelet enable 2>/dev/null || true + echo "# procd init script edgelet installed (engine=${_eng})" + ;; + sysvinit) + write_sysvinit_edgelet + if command -v update-rc.d >/dev/null 2>&1; then + maybe_sudo update-rc.d edgelet defaults 2>/dev/null || true + elif command -v chkconfig >/dev/null 2>&1; then + maybe_sudo chkconfig --add edgelet 2>/dev/null || true + fi + echo "# SysV init script edgelet installed" + ;; + upstart) + write_upstart_edgelet + echo "# Upstart job edgelet installed" + ;; + s6) + mkdir -p /etc/s6/edgelet + write_s6_run + write_s6_finish + echo "# s6 service installed under /etc/s6/edgelet" + ;; + runit) + mkdir -p /etc/runit/edgelet + write_runit_run + write_runit_finish + if [ -d /etc/runit ]; then + ln -sf /etc/runit/edgelet /etc/service/edgelet 2>/dev/null || ln -sf /etc/runit/edgelet /var/service/edgelet 2>/dev/null || true + fi + echo "# runit service installed under /etc/runit/edgelet" + ;; + launchd) + echo "# launchd native install deferred to start_edgelet.sh on darwin" + ;; + windows) + echo "# windows init unit stubs: use edgelet.exe service tooling when available" + ;; + *) + echo "Error: no supported init system detected (${_init})" + exit 1 + ;; + esac +} + +case "$EDGELET_INSTALL_MODE" in + container) install_container_init_units ;; + native|"") install_native_init_units ;; + *) echo "Error: unknown EDGELET_INSTALL_MODE=$EDGELET_INSTALL_MODE"; exit 1 ;; +esac diff --git a/assets/edgelet/scripts/lib/binary.sh b/assets/edgelet/scripts/lib/binary.sh new file mode 100644 index 000000000..c21b4ba90 --- /dev/null +++ b/assets/edgelet/scripts/lib/binary.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# Binary download, verification, and install helpers. + +GITHUB_REPO="${EDGELET_GITHUB_REPO:-eclipse-iofog/edgelet}" + +release_download_url() { + _ver="$1" + _os="$2" + _arch="$3" + echo "https://github.com/${GITHUB_REPO}/releases/download/${_ver}/$(binary_basename "$_os" "$_arch")" +} + +verify_binary_checksum() { + _bin="$1" + [ -f "$_bin" ] || die "Not a file: $_bin" + if [ -n "$EXPECTED_SHA256" ]; then + _sum=$(sha256_file "$_bin") + [ "$_sum" = "$EXPECTED_SHA256" ] || die "SHA256 mismatch (expected $EXPECTED_SHA256 got $_sum)" + info "SHA256 verified." + elif [ -n "$CHECKSUM_FILE" ] && [ -f "$CHECKSUM_FILE" ]; then + _bn=$(basename "$_bin") + ( cd "$(dirname "$_bin")" && grep " ${_bn}\$" "$CHECKSUM_FILE" >/dev/null ) || \ + ( cd "$(dirname "$CHECKSUM_FILE")" && sha256sum -c "$CHECKSUM_FILE" ) || \ + die "Checksum file verification failed" + fi +} + +download_or_stage_binary() { + _dest="$1" + if [ -n "$BIN_PATH" ]; then + [ -f "$BIN_PATH" ] || die "Local binary not found: $BIN_PATH" + verify_binary_checksum "$BIN_PATH" + cp "$BIN_PATH" "$_dest" + info "Using local binary: ${BIN_PATH}" + return 0 + fi + if [ "$AIRGAP" = true ]; then + die "--airgap requires --bin-path" + fi + _url=$(release_download_url "$EDGELET_VERSION" "$OS" "$ARCH") + info "Downloading ${_url} ..." + curl -fsSL -o "$_dest" "$_url" || die "Failed to download release binary" +} + +compute_source_url() { + if [ -n "$BIN_PATH" ]; then + _real=$(cd "$(dirname "$BIN_PATH")" && pwd)/$(basename "$BIN_PATH") + echo "file://${_real}" + else + release_download_url "$EDGELET_VERSION" "$OS" "$ARCH" + fi +} + +install_binary_file() { + _src="$1" + _dest="$2" + _dir=$(dirname "$_dest") + maybe_sudo mkdir -p "$_dir" + maybe_sudo install -m 755 "$_src" "$_dest" + info "Installed ${_dest}" +} + +fetch_latest_version() { + _ver=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" \ + | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') || true + [ -n "$_ver" ] || die "Failed to determine latest version" + echo "$_ver" +} + +stop_edgelet_daemon_desktop() { + _os="$1" + if pgrep -f '[e]dgelet daemon' >/dev/null 2>&1; then + pkill -f '[e]dgelet daemon' 2>/dev/null || true + sleep 1 + info "edgelet daemon stopped." + fi + case "${_os}" in + darwin) rm -f /var/run/edgelet/edgelet.pid ;; + windows) rm -f "$(windows_program_data_edgelet)/run/edgelet.pid" ;; + esac +} diff --git a/assets/edgelet/scripts/lib/common.sh b/assets/edgelet/scripts/lib/common.sh new file mode 100644 index 000000000..2747b0a60 --- /dev/null +++ b/assets/edgelet/scripts/lib/common.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# Shared helpers for potctl edgelet install scripts. + +die() { echo "ERROR: $1" >&2; exit 1; } +info() { echo ">>> $1"; } + +edgelet_host_os() { + case "${EDGELET_OS:-}" in + darwin|windows|linux) + echo "$EDGELET_OS" + ;; + *) + case "$(uname -s)" in + Darwin) echo darwin ;; + MINGW*|MSYS*|CYGWIN*) echo windows ;; + *) echo linux ;; + esac + ;; + esac +} + +is_desktop_container_host() { + case "$(edgelet_host_os)" in + darwin|windows) return 0 ;; + *) return 1 ;; + esac +} + +# desktop_container_local is true for potctl local install of container edgelet on darwin/windows. +desktop_container_local() { + [ "${LOCAL_INSTALL:-0}" = "1" ] \ + && [ "${EDGELET_INSTALL_MODE:-native}" = "container" ] \ + && is_desktop_container_host +} + +maybe_sudo() { + if desktop_container_local; then + "$@" + return + fi + if [ "$(id -u)" -eq 0 ]; then + "$@" + else + sudo "$@" + fi +} diff --git a/assets/edgelet/scripts/lib/container_cli.sh b/assets/edgelet/scripts/lib/container_cli.sh new file mode 100644 index 000000000..606c87e56 --- /dev/null +++ b/assets/edgelet/scripts/lib/container_cli.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# Host CLI wrapper for containerized edgelet (docker/podman exec into running container). + +install_container_cli_wrapper() { + _engine="$1" + _container="${2:-edgelet}" + _os="${3:-linux}" + + case "$_engine" in + docker|podman) ;; + *) die "container CLI wrapper requires docker or podman engine (got $_engine)" ;; + esac + + case "$_os" in + linux|darwin) ;; + *) die "container CLI wrapper is unsupported on os=$_os" ;; + esac + + if desktop_container_local; then + _stage="${EDGELET_SCRIPT_STAGE_DIR:-}" + [ -n "$_stage" ] || die "EDGELET_SCRIPT_STAGE_DIR is required for desktop local container install" + _dest="${_stage}/bin/edgelet" + mkdir -p "${_stage}/bin" + cat < "$_dest" +#!/bin/sh +CONTAINER_NAME="${_container}" +ENGINE="${_engine}" + +if [ "\$1" = "daemon" ]; then + echo "Error: edgelet daemon is managed by the \${ENGINE} container service." >&2 + exit 1 +fi + +if ! "\$ENGINE" ps --format '{{.Names}}' 2>/dev/null | grep -q "^\${CONTAINER_NAME}\$"; then + echo "Error: The edgelet container is not running." >&2 + exit 1 +fi +exec "\$ENGINE" exec "\$CONTAINER_NAME" edgelet "\$@" +EOF + chmod 755 "$_dest" + info "Installed container CLI wrapper at ${_dest} (engine=${_engine} container=${_container})" + return 0 + fi + + _dest="$(binary_path_for_os "$_os")" + maybe_sudo mkdir -p "$(dirname "$_dest")" + cat </dev/null +#!/bin/sh +CONTAINER_NAME="${_container}" +ENGINE="${_engine}" + +if [ "\$1" = "daemon" ]; then + echo "Error: edgelet daemon is managed by the \${ENGINE} container service." >&2 + exit 1 +fi + +if ! "\$ENGINE" ps --format '{{.Names}}' 2>/dev/null | grep -q "^\${CONTAINER_NAME}\$"; then + echo "Error: The edgelet container is not running." >&2 + exit 1 +fi +exec "\$ENGINE" exec "\$CONTAINER_NAME" edgelet "\$@" +EOF + maybe_sudo chmod 755 "$_dest" + info "Installed container CLI wrapper at ${_dest} (engine=${_engine} container=${_container})" +} diff --git a/assets/edgelet/scripts/lib/container_engine.sh b/assets/edgelet/scripts/lib/container_engine.sh new file mode 100644 index 000000000..fd9c4b05e --- /dev/null +++ b/assets/edgelet/scripts/lib/container_engine.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# Container engine socket URL/path helpers (aligned with pkg/iofog/install/edgelet_config.go). + +if ! type die >/dev/null 2>&1; then + die() { echo "ERROR: $1" >&2; exit 1; } +fi + +default_container_engine_url() { + _engine=$(echo "$1" | tr '[:upper:]' '[:lower:]') + case "$_engine" in + docker) echo "unix:///var/run/docker.sock" ;; + podman) echo "unix:///run/podman/podman.sock" ;; + *) echo "unix:///run/edgelet/containerd.sock" ;; + esac +} + +unix_url_to_path() { + _url="$1" + case "$_url" in + unix://*) echo "${_url#unix://}" ;; + *) echo "$_url" ;; + esac +} + +read_edgelet_config_profile_value() { + _key="$1" + _file="${EDGELET_CONFIG_FILE:-/etc/edgelet/config.yaml}" + [ -f "$_file" ] || return 1 + awk -v key="$_key" ' + /^ production:/ { p=1; next } + p && /^ [a-zA-Z]/ && !/^ production:/ { exit } + p && index($0, " " key ":") == 1 { + sub(/^ [^:]*:[[:space:]]*/, "") + gsub(/^"/, ""); gsub(/"$/, "") + gsub(/^'\''/, ""); gsub(/'\''$/, "") + print + exit + } + ' "$_file" +} + +resolve_container_engine_sock_url() { + if [ -n "${EDGELET_CONTAINER_ENGINE_URL:-}" ]; then + echo "$EDGELET_CONTAINER_ENGINE_URL" + return 0 + fi + _url=$(read_edgelet_config_profile_value "containerEngineUrl" 2>/dev/null) || true + if [ -n "$_url" ]; then + echo "$_url" + return 0 + fi + _engine="${CONTAINER_ENGINE:-docker}" + _cfg_engine=$(read_edgelet_config_profile_value "containerEngine" 2>/dev/null) || true + if [ -n "$_cfg_engine" ]; then + _engine="$_cfg_engine" + fi + default_container_engine_url "$_engine" +} + +resolve_container_engine_sock_path() { + unix_url_to_path "$(resolve_container_engine_sock_url)" +} + +container_engine_sock_mount() { + _path=$(resolve_container_engine_sock_path) + [ -n "$_path" ] || die "container engine socket path is empty" + echo "-v ${_path}:${_path}:rw" +} diff --git a/assets/edgelet/scripts/lib/container_mounts.sh b/assets/edgelet/scripts/lib/container_mounts.sh new file mode 100644 index 000000000..9dbe72a83 --- /dev/null +++ b/assets/edgelet/scripts/lib/container_mounts.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# Container volume/bind helpers for edgelet container deployment. +# Requires lib/common.sh (is_desktop_container_host, maybe_sudo). + +edgelet_container_volume_mounts() { + echo "-v /etc/edgelet:/etc/edgelet:rw \ +-v /var/lib/edgelet:/var/lib/edgelet:rw \ +-v /var/lib/edgelet-containerd:/var/lib/edgelet-containerd:rw \ +-v /var/log/edgelet:/var/log/edgelet:rw \ +-v /tmp/edgelet:/tmp/edgelet:rw" +} + +prepare_edgelet_container_host_dirs() { + if is_desktop_container_host; then + echo "# skipping host FHS prep on ${EDGELET_OS} container deploy (VM-local bind paths)" + return 0 + fi + maybe_sudo mkdir -p /etc/edgelet /var/lib/edgelet /var/lib/edgelet-containerd \ + /var/log/edgelet /var/run/edgelet /run/edgelet /tmp/edgelet +} + +container_runtime_bin() { + case "${CONTAINER_ENGINE:-docker}" in + docker) echo "docker" ;; + podman) echo "podman" ;; + *) die "container deployment requires docker or podman engine (got ${CONTAINER_ENGINE:-})" ;; + esac +} + +restart_edgelet_container() { + _run=$(container_runtime_bin) + _name="${EDGELET_CONTAINER_NAME:-edgelet}" + info "Restarting edgelet container ${_name}" + maybe_sudo "$_run" restart "$_name" +} diff --git a/assets/edgelet/scripts/lib/paths.sh b/assets/edgelet/scripts/lib/paths.sh new file mode 100644 index 000000000..e4037d2bf --- /dev/null +++ b/assets/edgelet/scripts/lib/paths.sh @@ -0,0 +1,89 @@ +#!/bin/sh +# OS-specific edgelet paths (aligned with upstream edgelet install.sh). + +windows_program_data_edgelet() { + echo "${ProgramData:-/c/ProgramData}/Edgelet" +} + +share_dir_for_os() { + case "$1" in + linux) echo "/usr/share/edgelet" ;; + darwin) echo "/usr/local/share/edgelet" ;; + windows) echo "$(windows_program_data_edgelet)/scripts" ;; + *) die "Unsupported OS for share dir: $1" ;; + esac +} + +binary_path_for_os() { + case "$1" in + linux|darwin) echo "/usr/local/bin/edgelet" ;; + windows) + _pf="${ProgramFiles:-/c/Program Files}" + echo "${_pf}/Edgelet/edgelet.exe" + ;; + *) die "Unsupported OS for binary path: $1" ;; + esac +} + +init_platform_paths() { + _os="$1" + case "${_os}" in + linux) + SHARE_DIR="/usr/share/edgelet" + CONFIG_DIR="/etc/edgelet" + RUNTIME_DIR="/run/edgelet" + ;; + darwin) + SHARE_DIR="/usr/local/share/edgelet" + CONFIG_DIR="/etc/edgelet" + RUNTIME_DIR="/var/run/edgelet" + ;; + windows) + _pd=$(windows_program_data_edgelet) + SHARE_DIR="${_pd}/scripts" + CONFIG_DIR="${_pd}/config" + RUNTIME_DIR="${_pd}/run" + ;; + *) die "Unsupported OS for platform paths: ${_os}" ;; + esac + CONFIG_FILE="${CONFIG_DIR}/config.yaml" + CERT_FILE="${CONFIG_DIR}/cert.crt" + export SHARE_DIR CONFIG_DIR CONFIG_FILE CERT_FILE RUNTIME_DIR +} + +install_dirs_for_os() { + _os="$1" + _share="$(share_dir_for_os "$_os")" + case "${_os}" in + linux) + maybe_sudo mkdir -p /etc/edgelet /var/log/edgelet /var/lib/edgelet /run/edgelet \ + /var/lib/edgelet-containerd /var/backups/edgelet /var/backups/edgelet/cache "$_share" + maybe_sudo chmod 750 /etc/edgelet /var/log/edgelet /var/lib/edgelet 2>/dev/null || true + ;; + darwin) + maybe_sudo mkdir -p /etc/edgelet /var/log/edgelet /var/lib/edgelet /var/lib/edgelet-containerd /var/run/edgelet \ + /var/backups/edgelet /var/backups/edgelet/cache "$_share" + maybe_sudo chmod 750 /etc/edgelet /var/log/edgelet /var/lib/edgelet 2>/dev/null || true + ;; + windows) + _pd=$(windows_program_data_edgelet) + maybe_sudo mkdir -p "${_pd}/data" "${_pd}/config" "${_pd}/run" "${_pd}/log" "${_pd}/scripts" 2>/dev/null || true + ;; + esac +} + +binary_basename() { + _os="$1" + _arch="$2" + case "${_os}" in + windows) echo "edgelet-${_os}-${_arch}.exe" ;; + *) echo "edgelet-${_os}-${_arch}" ;; + esac +} + +default_container_engine_for_os() { + case "$1" in + linux) echo "edgelet" ;; + *) echo "docker" ;; + esac +} diff --git a/assets/edgelet/scripts/lib/receipt.sh b/assets/edgelet/scripts/lib/receipt.sh new file mode 100644 index 000000000..e7ca5ca59 --- /dev/null +++ b/assets/edgelet/scripts/lib/receipt.sh @@ -0,0 +1,94 @@ +#!/bin/sh +# Install receipt and rollback metadata (/var/backups/edgelet). + +BACKUP_DIR="${BACKUP_DIR:-/var/backups/edgelet}" +CACHE_DIR="${CACHE_DIR:-${BACKUP_DIR}/cache}" +RECEIPT_FILE="${RECEIPT_FILE:-${BACKUP_DIR}/install-receipt}" +PREVIOUS_FILE="${PREVIOUS_FILE:-${BACKUP_DIR}/previous-release}" + +sha256_file() { + sha256sum "$1" | awk '{print $1}' +} + +kv_get() { + _file="$1" + _key="$2" + [ -f "$_file" ] || { echo ""; return 0; } + _line=$(grep "^${_key}=" "$_file" | head -1) || true + [ -n "$_line" ] || { echo ""; return 0; } + echo "$_line" | sed "s/^${_key}=//" +} + +write_install_receipt() { + _ver="$1" + _os="$2" + _arch="$3" + _eng="$4" + _url="$5" + _sha="$6" + _method="$7" + maybe_sudo mkdir -p "$BACKUP_DIR" + _ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u) + { + printf 'installed_version=%s\n' "$_ver" + printf 'os=%s\n' "$_os" + printf 'arch=%s\n' "$_arch" + printf 'container_engine=%s\n' "$_eng" + printf 'source_url=%s\n' "$_url" + printf 'installed_at=%s\n' "$_ts" + printf 'install_method=%s\n' "$_method" + printf 'binary_sha256=%s\n' "$_sha" + } | maybe_sudo tee "$RECEIPT_FILE" >/dev/null + maybe_sudo chmod 600 "$RECEIPT_FILE" 2>/dev/null || true +} + +write_previous_release() { + _pv="$1" + _pos="$2" + _parch="$3" + _peng="$4" + _purl="$5" + _psha="$6" + _cfg="$7" + maybe_sudo mkdir -p "$BACKUP_DIR" + { + printf 'previous_version=%s\n' "$_pv" + printf 'previous_os=%s\n' "$_pos" + printf 'previous_arch=%s\n' "$_parch" + printf 'previous_container_engine=%s\n' "$_peng" + printf 'previous_download_url=%s\n' "$_purl" + printf 'previous_binary_sha256=%s\n' "$_psha" + printf 'config_backup_path=%s\n' "$_cfg" + } | maybe_sudo tee "$PREVIOUS_FILE" >/dev/null + maybe_sudo chmod 600 "$PREVIOUS_FILE" 2>/dev/null || true +} + +cache_binary() { + _ver="$1" + _os="$2" + _arch="$3" + _src="$4" + maybe_sudo mkdir -p "$CACHE_DIR" + _dest="${CACHE_DIR}/edgelet-${_ver}-${_os}-${_arch}" + case "${_os}" in + windows) _dest="${_dest}.exe" ;; + esac + maybe_sudo cp "$_src" "$_dest" + maybe_sudo chmod 755 "$_dest" 2>/dev/null || true + info "Cached binary at ${_dest}" +} + +cached_binary_path() { + _ver="$1" + _os="$2" + _arch="$3" + _p="${CACHE_DIR}/edgelet-${_ver}-${_os}-${_arch}" + case "${_os}" in + windows) _p="${_p}.exe" ;; + esac + if [ -f "$_p" ]; then + echo "$_p" + return 0 + fi + echo "" +} diff --git a/assets/edgelet/scripts/start_edgelet.sh b/assets/edgelet/scripts/start_edgelet.sh new file mode 100755 index 000000000..7f3b139be --- /dev/null +++ b/assets/edgelet/scripts/start_edgelet.sh @@ -0,0 +1,183 @@ +#!/bin/sh +# Enable/start edgelet daemon or container deployment unit. +set -e +set -x + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/lib/common.sh" +. "$SCRIPT_DIR/lib/container_engine.sh" +. "$SCRIPT_DIR/lib/container_mounts.sh" +EDGELET_DETECT_SOURCED=1 +. "$SCRIPT_DIR/detect_init.sh" +init + +CONTAINER_ENGINE="${CONTAINER_ENGINE:-edgelet}" +EDGELET_INSTALL_MODE="${EDGELET_INSTALL_MODE:-native}" +EDGELET_CONTAINER_IMAGE="${EDGELET_CONTAINER_IMAGE:-}" +EDGELET_TZ="${EDGELET_TZ:-UTC}" +EDGELET_CONTAINER_NAME="${EDGELET_CONTAINER_NAME:-edgelet}" + +start_native_linux() { + case "$INIT_SYSTEM" in + systemd) + if [ "$CONTAINER_ENGINE" = "edgelet" ]; then + maybe_sudo systemctl start edgelet-containerd 2>/dev/null || true + fi + maybe_sudo systemctl reset-failed edgelet 2>/dev/null || true + maybe_sudo systemctl start edgelet + ;; + openrc) + if [ "$CONTAINER_ENGINE" = "edgelet" ]; then + maybe_sudo rc-service edgelet-containerd start 2>/dev/null || true + fi + maybe_sudo rc-service edgelet restart 2>/dev/null || maybe_sudo rc-service edgelet start + ;; + procd) + maybe_sudo /etc/init.d/edgelet start + ;; + sysvinit) + maybe_sudo /etc/init.d/edgelet restart 2>/dev/null || maybe_sudo /etc/init.d/edgelet start + ;; + upstart) + maybe_sudo initctl restart edgelet 2>/dev/null || maybe_sudo initctl start edgelet + ;; + s6) + if command -v s6-svc >/dev/null 2>&1 && [ -d /var/run/s6/services/edgelet ]; then + maybe_sudo s6-svc -u /var/run/s6/services/edgelet 2>/dev/null || true + fi + ;; + runit) + maybe_sudo sv restart edgelet 2>/dev/null || maybe_sudo sv start edgelet 2>/dev/null || true + ;; + *) + echo "Error: cannot start edgelet on init=$INIT_SYSTEM" + exit 1 + ;; + esac +} + +edgelet_daemon_running() { + pgrep -f '[e]dgelet daemon' >/dev/null 2>&1 +} + +start_edgelet_daemon_desktop() { + _log_dir="/var/log/edgelet" + _log_file="${_log_dir}/daemon.log" + _pid_file="/var/run/edgelet/edgelet.pid" + case "$EDGELET_OS" in + darwin) + maybe_sudo mkdir -p "$_log_dir" /var/run/edgelet + ;; + windows) + die "windows native start must be handled by platform-specific tooling" + ;; + *) return 0 ;; + esac + + if edgelet_daemon_running; then + echo "# edgelet daemon already running" + return 0 + fi + + # Detach under one root shell so the daemon survives bootstrap script exit. + maybe_sudo sh -c "nohup edgelet daemon >> '${_log_file}' 2>&1 '${_pid_file}'" + + _iter=0 + _max="${EDGELET_START_TIMEOUT:-60}" + while [ "$_iter" -lt "$_max" ]; do + if edgelet_daemon_running; then + break + fi + sleep 1 + _iter=$((_iter + 1)) + done + + if ! edgelet_daemon_running; then + echo "ERROR: edgelet daemon failed to start within ${_max}s; see ${_log_file}" >&2 + if [ -f "$_log_file" ]; then + tail -20 "$_log_file" >&2 || true + fi + exit 1 + fi + + _pid=$(cat "$_pid_file" 2>/dev/null || pgrep -f '[e]dgelet daemon' | head -1) + echo "# edgelet daemon started in background (pid=${_pid:-unknown}, log=${_log_file})" +} + +start_container_linux() { + case "$INIT_SYSTEM" in + systemd) + maybe_sudo systemctl start edgelet + ;; + openrc) + maybe_sudo rc-service edgelet start + ;; + *) + echo "Error: cannot start container deployment on init=$INIT_SYSTEM" + exit 1 + ;; + esac +} + +start_container_desktop() { + _image="${EDGELET_CONTAINER_IMAGE:-}" + if [ -z "$_image" ]; then + echo "Error: EDGELET_CONTAINER_IMAGE is required" + exit 1 + fi + _run=$(container_runtime_bin) + prepare_edgelet_container_host_dirs + if $_run ps --format '{{.Names}}' 2>/dev/null | grep -q "^${EDGELET_CONTAINER_NAME}$"; then + echo "# edgelet container already running" + return 0 + fi + _sock_mount=$(container_engine_sock_mount) + _vol_mounts=$(edgelet_container_volume_mounts) + # shellcheck disable=SC2086 + maybe_sudo $_run run -d --name "${EDGELET_CONTAINER_NAME}" \ + -e "TZ=${EDGELET_TZ}" \ + -e "EDGELET_DAEMON=container" \ + ${_sock_mount} \ + ${_vol_mounts} \ + --net=host --privileged --stop-timeout 60 \ + --restart=always \ + "$_image" + echo "# edgelet container started (${EDGELET_CONTAINER_NAME})" +} + +start_container_darwin() { + start_container_desktop +} + +start_container_windows() { + start_container_desktop +} + +case "$EDGELET_INSTALL_MODE" in + container) + case "$EDGELET_OS" in + linux) start_container_linux ;; + darwin) start_container_darwin ;; + windows) start_container_windows ;; + *) echo "Error: container deployment start not supported on os=$EDGELET_OS"; exit 1 ;; + esac + ;; + native|"") + case "$EDGELET_OS" in + linux) start_native_linux ;; + darwin) start_edgelet_daemon_desktop ;; + windows) + echo "Error: windows native start must be handled by platform-specific tooling" + exit 1 + ;; + *) + echo "Error: unsupported os=$EDGELET_OS" + exit 1 + ;; + esac + ;; + *) + echo "Error: unknown EDGELET_INSTALL_MODE=$EDGELET_INSTALL_MODE" + exit 1 + ;; +esac diff --git a/assets/edgelet/scripts/uninstall.sh b/assets/edgelet/scripts/uninstall.sh new file mode 100755 index 000000000..34e991476 --- /dev/null +++ b/assets/edgelet/scripts/uninstall.sh @@ -0,0 +1,314 @@ +#!/bin/sh +# uninstall.sh — Edgelet uninstaller +# +# Usage: +# sudo sh uninstall.sh [--remove-data] +# +# --remove-data also removes config, data, runtime, logs, and backup directories + +set -e + +die() { echo "ERROR: $1" >&2; exit 1; } +info() { echo ">>> $1"; } + +detect_os() { + _u=$(uname -s) + case "${_u}" in + Linux) echo "linux" ;; + Darwin) echo "darwin" ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) echo "windows" ;; + *) die "Unsupported OS: ${_u}" ;; + esac +} + +windows_program_data_edgelet() { + echo "${ProgramData:-/c/ProgramData}/Edgelet" +} + +share_dir_for_os() { + case "$1" in + linux) echo "/usr/share/edgelet" ;; + darwin) echo "/usr/local/share/edgelet" ;; + windows) echo "$(windows_program_data_edgelet)/scripts" ;; + *) die "Unsupported OS: $1" ;; + esac +} + +binary_path_for_os() { + case "$1" in + linux|darwin) echo "/usr/local/bin/edgelet" ;; + windows) + _pf="${ProgramFiles:-/c/Program Files}" + echo "${_pf}/Edgelet/edgelet.exe" + ;; + *) die "Unsupported OS: $1" ;; + esac +} + +# OpenRC ships /sbin/openrc-run on Alpine even when PID 1 is busybox (Lima +# template:alpine). Require a running OpenRC supervisor, not merely openrc-run. +openrc_is_pid1() { + [ -x /sbin/openrc-run ] || return 1 + if rc-status -s >/dev/null 2>&1; then + return 0 + fi + if [ -f /etc/inittab ] && grep -q '/sbin/openrc' /etc/inittab 2>/dev/null; then + return 0 + fi + _init="$(readlink -f /sbin/init 2>/dev/null || readlink /sbin/init 2>/dev/null || true)" + case "${_init}" in + *openrc*) return 0 ;; + esac + return 1 +} + +procd_is_openwrt() { + [ -x /sbin/procd ] && [ -f /etc/rc.common ] || return 1 + return 0 +} + +detect_init() { + if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then + echo "systemd" + return 0 + fi + if procd_is_openwrt; then + echo "procd" + return 0 + fi + if openrc_is_pid1 && { + command -v openrc >/dev/null 2>&1 \ + || [ -x /sbin/openrc-run ] \ + || [ -f /sbin/openrc ] + }; then + echo "openrc" + return 0 + fi + if command -v initctl >/dev/null 2>&1 && [ -d /etc/init ]; then + echo "upstart" + return 0 + fi + if [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then + echo "s6" + return 0 + fi + if command -v runsvdir >/dev/null 2>&1 || [ -d /etc/runit ]; then + echo "runit" + return 0 + fi + if [ -f /etc/inittab ] || command -v update-rc.d >/dev/null 2>&1 || command -v chkconfig >/dev/null 2>&1; then + echo "sysvinit" + return 0 + fi + echo "unknown" +} + +[ "$(id -u)" -eq 0 ] || die "Must be run as root. Try: sudo $0 $*" + +OS=$(detect_os) +SHARE_DIR=$(share_dir_for_os "$OS") +BINARY_PATH=$(binary_path_for_os "$OS") + +REMOVE_DATA=false +for arg in "$@"; do + case "${arg}" in + --remove-data) REMOVE_DATA=true ;; + --help|-h) + echo "Usage: $0 [--remove-data]" + echo "" + echo " --remove-data also delete config, data, and backup directories" + exit 0 ;; + *) die "Unknown option: ${arg}" ;; + esac +done + +stop_systemd() { + if systemctl is-active --quiet edgelet-containerd 2>/dev/null; then + systemctl stop edgelet-containerd + fi + if systemctl is-active --quiet edgelet 2>/dev/null; then + systemctl stop edgelet + fi + systemctl disable edgelet edgelet-containerd 2>/dev/null || true + rm -f /etc/systemd/system/edgelet.service + rm -f /etc/systemd/system/edgelet-containerd.service + rm -rf /etc/systemd/system/edgelet.service.d + systemctl daemon-reload 2>/dev/null || true + info "systemd service removed." +} + +stop_procd() { + /etc/init.d/edgelet stop 2>/dev/null || true + /etc/init.d/edgelet disable 2>/dev/null || true + rm -f /etc/init.d/edgelet + info "procd init script removed." +} + +stop_openrc() { + rc-service edgelet-containerd stop 2>/dev/null || true + rc-service edgelet stop 2>/dev/null || true + rc-update del edgelet-containerd default 2>/dev/null || true + rc-update del edgelet default 2>/dev/null || true + rm -f /etc/init.d/edgelet /etc/init.d/edgelet-containerd + info "OpenRC service removed." +} + +stop_sysvinit() { + /etc/init.d/edgelet stop 2>/dev/null || true + update-rc.d -f edgelet remove 2>/dev/null || true + chkconfig --del edgelet 2>/dev/null || true + rm -f /etc/init.d/edgelet + info "SysV init service removed." +} + +stop_upstart() { + initctl stop edgelet 2>/dev/null || true + rm -f /etc/init/edgelet.conf + initctl reload-configuration 2>/dev/null || true + info "Upstart service removed." +} + +stop_s6() { + s6-svc -d /var/run/s6/services/edgelet 2>/dev/null || true + rm -rf /var/run/s6/services/edgelet + rm -rf /etc/s6/edgelet + info "s6 service removed." +} + +stop_runit() { + sv down edgelet 2>/dev/null || true + rm -f /etc/service/edgelet /var/service/edgelet /service/edgelet 2>/dev/null || true + rm -rf /etc/runit/edgelet + info "runit service removed." +} + +stop_fallback() { + pkill -f "/usr/local/bin/edgelet daemon" 2>/dev/null || true + pkill -f "edgelet daemon" 2>/dev/null || true + info "Background edgelet processes stopped (if any)." +} + +lazy_umount_edgelet() { + if ! command -v umount >/dev/null 2>&1; then + return 0 + fi + mount 2>/dev/null | grep -E '/run/edgelet|/var/run/edgelet|/var/lib/edgelet' | awk '{print $3}' | \ + sort -r | while read -r mp; do + [ -n "$mp" ] || continue + umount -l "${mp}" 2>/dev/null || true + done +} + +remove_init_service() { + _init=$(detect_init 2>/dev/null) || _init="unknown" + if [ -f /etc/systemd/system/edgelet.service ]; then + stop_systemd + elif [ -f /etc/init.d/edgelet ] && [ -x /sbin/procd ]; then + stop_procd + elif [ -f /etc/init.d/edgelet ] && command -v openrc >/dev/null 2>&1; then + stop_openrc + elif [ -f /etc/init/edgelet.conf ]; then + stop_upstart + elif [ -f /etc/init.d/edgelet ]; then + stop_sysvinit + elif [ -d /etc/s6/edgelet ]; then + stop_s6 + elif [ -d /etc/runit/edgelet ] || [ -L /var/service/edgelet ]; then + stop_runit + elif [ "$_init" != "" ] && [ "$_init" != "unknown" ]; then + case "$_init" in + systemd) stop_systemd ;; + openrc) stop_openrc ;; + procd) stop_procd ;; + sysvinit) stop_sysvinit ;; + upstart) stop_upstart ;; + s6) stop_s6 ;; + runit) stop_runit ;; + *) stop_fallback ;; + esac + else + stop_fallback + fi +} + +remove_init_service +lazy_umount_edgelet + +# Container deployment instance +EDGELET_CONTAINER_NAME="${EDGELET_CONTAINER_NAME:-edgelet}" +docker rm -f "$EDGELET_CONTAINER_NAME" 2>/dev/null || true +podman rm -f "$EDGELET_CONTAINER_NAME" 2>/dev/null || true + +rm -f "$BINARY_PATH" +info "Binary removed." + +case "$OS" in + linux) + rm -rf /usr/libexec/edgelet + info "Init helpers removed from /usr/libexec/edgelet/" + ;; +esac + +rm -rf "$SHARE_DIR" +info "Bundled scripts removed from ${SHARE_DIR}/" + +if [ "${REMOVE_DATA}" = "true" ]; then + info "Removing agent data directories..." + lazy_umount_edgelet + case "$OS" in + linux) + rm -rf /var/lib/edgelet + rm -rf /var/lib/edgelet-containerd + rm -rf /run/edgelet + rm -rf /var/log/edgelet + rm -rf /var/backups/edgelet + rm -rf /etc/edgelet + ;; + darwin) + rm -rf /var/lib/edgelet + rm -rf /var/run/edgelet + rm -rf /var/log/edgelet + rm -rf /var/backups/edgelet + rm -rf /etc/edgelet + ;; + windows) + _pd=$(windows_program_data_edgelet) + rm -rf "${_pd}/data" + rm -rf "${_pd}/config" + rm -rf "${_pd}/run" + rm -rf "${_pd}/log" + rm -rf "${_pd}/scripts" + rm -rf "${_pd}" + ;; + esac + info "Data, backups, and configuration removed." +else + info "Data directories preserved (use --remove-data to remove):" + case "$OS" in + linux) + info " /var/lib/edgelet" + info " /var/lib/edgelet-containerd" + info " /run/edgelet" + info " /var/log/edgelet" + info " /var/backups/edgelet" + info " /etc/edgelet" + ;; + darwin) + info " /var/lib/edgelet" + info " /var/run/edgelet" + info " /var/log/edgelet" + info " /var/backups/edgelet" + info " /etc/edgelet" + ;; + windows) + _pd=$(windows_program_data_edgelet) + info " ${_pd}/data" + info " ${_pd}/config" + info " ${_pd}/run" + info " ${_pd}/log" + ;; + esac +fi + +info "" +info "Edgelet has been uninstalled." diff --git a/assets/edgelet/scripts/wait_edgelet_ready.sh b/assets/edgelet/scripts/wait_edgelet_ready.sh new file mode 100644 index 000000000..51d9aa003 --- /dev/null +++ b/assets/edgelet/scripts/wait_edgelet_ready.sh @@ -0,0 +1,106 @@ +#!/bin/sh +# Wait until edgelet init services and daemon API report iofogDaemon: RUNNING. +set -e + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/lib/common.sh" + +EDGELET_DETECT_SOURCED=1 +. "$SCRIPT_DIR/detect_init.sh" +init + +wait_init_services() { + case "$INIT_SYSTEM" in + systemd) + maybe_sudo systemctl is-enabled edgelet >/dev/null 2>&1 || return 0 + _iter=0 + while [ "$_iter" -lt 120 ]; do + if maybe_sudo systemctl is-active edgelet >/dev/null 2>&1; then + return 0 + fi + sleep 1 + _iter=$((_iter + 1)) + done + ;; + openrc) + maybe_sudo rc-service edgelet status >/dev/null 2>&1 && return 0 + ;; + procd) + [ -x /etc/init.d/edgelet ] && return 0 + ;; + launchd|none) + return 0 + ;; + esac + return 0 +} + +edgelet_status_running() { + _out="$1" + if echo "$_out" | grep -qi 'daemon is not running'; then + return 1 + fi + if echo "$_out" | grep -q 'Edgelet API is still initializing'; then + return 1 + fi + if echo "$_out" | grep -q 'iofogDaemon: RUNNING'; then + return 0 + fi + if echo "$_out" | grep -q 'runtime.agentPhase: running'; then + return 0 + fi + return 1 +} + +wait_edgelet_api_desktop_container() { + _iter=0 + _max="${EDGELET_READY_TIMEOUT:-600}" + while [ "$_iter" -lt "$_max" ]; do + _out="" + _out=$(edgelet system status 2>&1) || true + if edgelet_status_running "$_out"; then + info "edgelet daemon is RUNNING" + return 0 + fi + echo "# waiting for edgelet RUNNING (${_iter}s)..." + sleep 1 + _iter=$((_iter + 1)) + done + die "Timed out after ${_max}s waiting for edgelet RUNNING" +} + +edgelet_daemon_running() { + pgrep -f '[e]dgelet daemon' >/dev/null 2>&1 +} + +wait_edgelet_api() { + if desktop_container_local; then + wait_edgelet_api_desktop_container + return 0 + fi + + _iter=0 + _max="${EDGELET_READY_TIMEOUT:-600}" + while [ "$_iter" -lt "$_max" ]; do + if ! edgelet_daemon_running; then + echo "# waiting for edgelet daemon process (${_iter}s)..." + sleep 1 + _iter=$((_iter + 1)) + continue + fi + _out="" + _out=$(maybe_sudo edgelet system status 2>&1) || true + if edgelet_status_running "$_out"; then + info "edgelet daemon is RUNNING" + return 0 + fi + _status=$(echo "$_out" | awk -F': ' '/^iofogDaemon:/ {print $2; exit}' | tr -d '[:space:]') + echo "# waiting for edgelet RUNNING (${_iter}s) status=${_status:-unknown}" + sleep 1 + _iter=$((_iter + 1)) + done + die "Timed out after ${_max}s waiting for edgelet RUNNING" +} + +wait_init_services +wait_edgelet_api diff --git a/assets/embed.go b/assets/embed.go new file mode 100644 index 000000000..965aa9631 --- /dev/null +++ b/assets/embed.go @@ -0,0 +1,8 @@ +package assets + +import "embed" + +// FS holds install scripts bundled with the CLI. +// +//go:embed edgelet +var FS embed.FS diff --git a/azure-pipelines.yaml b/azure-pipelines.yaml deleted file mode 100644 index 2c27a386b..000000000 --- a/azure-pipelines.yaml +++ /dev/null @@ -1,188 +0,0 @@ -# trigger: -# tags: -# include: -# - v* -# branches: -# include: -# - develop -# - master -# paths: -# exclude: -# - README.md -# - CHANGELOG.md -# - LICENSE -# - docs/* - -# variables: -# build: $(Build.BuildId) -# jobuuid: $(Build.BuildId)$(Agent.Id) -# GOROOT: '/usr/local/go1.18' -# GOPATH: '/tmp/go' -# GOBIN: '$(GOPATH)/bin' -# ref: $(Build.SourceBranch) -# branch: $(Build.SourceBranchName) -# controller_image: 'gcr.io/focal-freedom-236620/controller:3.0.4' -# enterprise_image: 'gcr.io/focal-freedom-236620/enterprise-controller:3.0.3' -# agent_image: 'gcr.io/focal-freedom-236620/agent:3.0.1' -# operator_image: 'gcr.io/focal-freedom-236620/operator:3.2.0' -# kubelet_image: 'gcr.io/focal-freedom-236620/kubelet:3.0.0-beta1' -# port_manager_image: 'gcr.io/focal-freedom-236620/port-manager:3.0.0' -# router_image: 'gcr.io/focal-freedom-236620/router:3.0.0' -# router_arm_image: 'gcr.io/focal-freedom-236620/router-arm:3.0.0' -# proxy_image: 'gcr.io/focal-freedom-236620/proxy:3.0.0' -# proxy_arm_image: 'gcr.io/focal-freedom-236620/proxy-arm:3.0.0' -# iofog_agent_version: '3.0.1' -# controller_version: '3.0.4' -# version: -# agent_vm_list: -# controller_vm: -# windows_ssh_key_path: 'C:/Users/$(azure.windows.user)/.ssh' -# ssh_key_file: 'id_rsa' -# windows_kube_config_path: 'C:/Users/$(azure.windows.user)/.kube/config' -# bash_kube_config_path: '/root/.kube/config' -# isTaggedCommit: $[startsWith(variables['Build.SourceBranch'], 'refs/tags/')] - -# stages: - -# - stage: Build -# jobs: -# - job: Build -# pool: -# vmImage: 'Ubuntu-20.04' -# steps: -# - template: pipeline/steps/prebuild.yaml -# - template: pipeline/steps/version.yaml -# - script: | -# set -e -# mkdir -p '$(GOBIN)' -# mkdir -p '$(GOPATH)/pkg' -# echo '##vso[task.prependpath]$(GOBIN)' -# echo '##vso[task.prependpath]$(GOROOT)/bin' -# displayName: 'Set up the Go workspace' -# - task: GoTool@0 -# inputs: -# version: '1.19' -# goPath: $(GOPATH) -# goBin: $(GOBIN) -# displayName: 'Install Golang' - -# - script: | -# set -e -# go install github.com/goreleaser/goreleaser@v1.1.0 -# displayName: 'iofogctl: Install Goreleaser' -# - script: | -# set -e -# goreleaser --snapshot --rm-dist --debug --config ./.goreleaser-iofogctl.yml -# displayName: 'iofogctl: Build packages' -# env: -# GITHUB_TOKEN: $(github_token) -# - task: PublishBuildArtifacts@1 -# condition: always() -# inputs: -# PathtoPublish: '$(System.DefaultWorkingDirectory)/dist' -# ArtifactName: iofogctl -# displayName: 'Publish iofogctl binaries' - -# - stage: Test -# jobs: -# - template: pipeline/win-k8s.yaml -# # - template: pipeline/win-vanilla.yaml -# - template: pipeline/local.yaml -# - template: pipeline/k8s.yaml -# - template: pipeline/ha.yaml -# - template: pipeline/vanilla.yaml -# parameters: -# job_name: Vanilla -# id: $(jobuuid) -# distro: $(gcp.vm.distro.bullseye) -# repo: $(gcp.vm.repo.debian) -# agent_count: 2 -# controller_count: 1 - -# - stage: Publish -# jobs: -# - job: Publish_iofogctl_Dev -# condition: or(and(succeeded(), eq(variables['build.sourceBranch'], 'refs/heads/develop')), and(succeeded(), startsWith(variables['build.sourceBranch'], 'refs/tags/'))) -# pool: -# vmImage: 'Ubuntu-22.04' -# steps: -# - template: pipeline/steps/version.yaml -# - script: | -# set -e -# mkdir -p '$(GOBIN)' -# mkdir -p '$(GOPATH)/pkg' -# echo '##vso[task.prependpath]$(GOBIN)' -# echo '##vso[task.prependpath]$(GOROOT)/bin' -# displayName: 'Set up the Go workspace' -# - task: GoTool@0 -# inputs: -# version: '1.19' -# goPath: $(GOPATH) -# goBin: $(GOBIN) -# displayName: 'Install Golang' - -# - script: | -# set -e -# go install github.com/goreleaser/goreleaser@v1.1.0 -# displayName: 'iofogctl: Install Goreleaser' -# - script: | -# go install github.com/edgeworx/packagecloud@v0.1.1 -# displayName: 'iofogctl: Install packagecloud CLI' -# - script: | -# set -e -# goreleaser --snapshot --rm-dist --debug --config ./.goreleaser-iofogctl-dev.yml -# ./.packagecloud-publish.sh -# displayName: 'iofogctl: Build and Release dev only packages' -# env: -# PACKAGECLOUD_TOKEN: $(packagecloud_token) -# PACKAGECLOUD_REPO: "iofog/iofogctl-snapshots" -# GITHUB_TOKEN: $(github_token) -# - task: PublishBuildArtifacts@1 -# condition: always() -# inputs: -# PathtoPublish: '$(System.DefaultWorkingDirectory)/dist' -# ArtifactName: iofogctl_dev -# displayName: 'Publish iofogctl binaries' - -# - job: Publish_iofogctl_Prod -# condition: and(succeeded(), eq(variables['isTaggedCommit'], true)) -# pool: -# vmImage: 'Ubuntu-22.04' -# steps: -# - template: pipeline/steps/version.yaml -# - script: | -# set -e -# mkdir -p '$(GOBIN)' -# mkdir -p '$(GOPATH)/pkg' -# echo '##vso[task.prependpath]$(GOBIN)' -# echo '##vso[task.prependpath]$(GOROOT)/bin' -# displayName: 'Set up the Go workspace' -# - task: GoTool@0 -# inputs: -# version: '1.19' -# goPath: $(GOPATH) -# goBin: $(GOBIN) -# displayName: 'Install Golang' - -# - script: | -# set -e -# go install github.com/goreleaser/goreleaser@v1.1.0 -# displayName: 'iofogctl: Install Goreleaser' -# - script: | -# go install github.com/edgeworx/packagecloud@v0.1.1 -# displayName: 'iofogctl: Install packagecloud CLI' -# - script: | -# set -e -# goreleaser --rm-dist --debug --config ./.goreleaser-iofogctl.yml -# ./.packagecloud-publish.sh -# displayName: 'iofogctl: Build and Release packages' -# env: -# PACKAGECLOUD_TOKEN: $(packagecloud_token) -# PACKAGECLOUD_REPO: "iofog/iofogctl" -# GITHUB_TOKEN: $(github_token) -# - task: PublishBuildArtifacts@1 -# condition: always() -# inputs: -# PathtoPublish: '$(System.DefaultWorkingDirectory)/dist' -# ArtifactName: iofogctl -# displayName: 'Publish iofogctl binaries' diff --git a/cmd/iofogctl/main.go b/cmd/iofogctl/main.go index 45c415082..29989add7 100644 --- a/cmd/iofogctl/main.go +++ b/cmd/iofogctl/main.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package main import ( diff --git a/cmd/potctl/main.go b/cmd/potctl/main.go new file mode 100644 index 000000000..29989add7 --- /dev/null +++ b/cmd/potctl/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/eclipse-iofog/iofogctl/internal/cmd" + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func main() { + config.Init("") + rootCmd := cmd.NewRootCommand() + err := rootCmd.Execute() + util.Check(err) +} diff --git a/docs/md/iofogctl.md b/docs/iofogctl_md/iofogctl.md similarity index 90% rename from docs/md/iofogctl.md rename to docs/iofogctl_md/iofogctl.md index d675f8b1e..cd35fc9b3 100644 --- a/docs/md/iofogctl.md +++ b/docs/iofogctl_md/iofogctl.md @@ -29,18 +29,17 @@ iofogctl [flags] * [iofogctl disconnect](iofogctl_disconnect.md) - Disconnect from an ioFog cluster * [iofogctl exec](iofogctl_exec.md) - Connect to an Exec Session of a resource * [iofogctl get](iofogctl_get.md) - Get information of existing resources -* [iofogctl legacy](iofogctl_legacy.md) - Execute commands using legacy CLI * [iofogctl logs](iofogctl_logs.md) - Get log contents of deployed resource * [iofogctl move](iofogctl_move.md) - Move an existing resources inside the current Namespace * [iofogctl nats](iofogctl_nats.md) - Manage NATS resources * [iofogctl prune](iofogctl_prune.md) - prune ioFog resources * [iofogctl rebuild](iofogctl_rebuild.md) - Rebuilds a microservice or system-microservice -* [iofogctl rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed +* [iofogctl reconcile](iofogctl_reconcile.md) - Retry async platform provisioning for an agent or service * [iofogctl rollback](iofogctl_rollback.md) - Rollback ioFog resources * [iofogctl start](iofogctl_start.md) - Starts a resource * [iofogctl stop](iofogctl_stop.md) - Stops a resource * [iofogctl upgrade](iofogctl_upgrade.md) - Upgrade ioFog resources * [iofogctl version](iofogctl_version.md) - Get CLI application version -* [iofogctl view](iofogctl_view.md) - Open ECN Viewer +* [iofogctl view](iofogctl_view.md) - Open EdgeOps Console diff --git a/docs/md/iofogctl_attach.md b/docs/iofogctl_md/iofogctl_attach.md similarity index 78% rename from docs/md/iofogctl_attach.md rename to docs/iofogctl_md/iofogctl_attach.md index 8e5d2fd3d..64737aa91 100644 --- a/docs/md/iofogctl_attach.md +++ b/docs/iofogctl_md/iofogctl_attach.md @@ -9,7 +9,7 @@ Attach one ioFog resource to another. ### Examples ``` -attach +iofogctl attach ``` ### Options @@ -30,8 +30,7 @@ attach * [iofogctl](iofogctl.md) - * [iofogctl attach agent](iofogctl_attach_agent.md) - Attach an Agent to an existing Namespace -* [iofogctl attach edge-resource](iofogctl_attach_edge-resource.md) - Attach an Edge Resource to an existing Agent -* [iofogctl attach exec](iofogctl_attach_exec.md) - Attach an Exec Session to a resource +* [iofogctl attach exec](iofogctl_attach_exec.md) - Provision fog debug exec on an Agent * [iofogctl attach volume-mount](iofogctl_attach_volume-mount.md) - Attach a Volume Mount to existing Agents diff --git a/docs/md/iofogctl_attach_agent.md b/docs/iofogctl_md/iofogctl_attach_agent.md similarity index 100% rename from docs/md/iofogctl_attach_agent.md rename to docs/iofogctl_md/iofogctl_attach_agent.md diff --git a/docs/md/iofogctl_attach_exec.md b/docs/iofogctl_md/iofogctl_attach_exec.md similarity index 66% rename from docs/md/iofogctl_attach_exec.md rename to docs/iofogctl_md/iofogctl_attach_exec.md index 7bc2bcb94..eaecd9252 100644 --- a/docs/md/iofogctl_attach_exec.md +++ b/docs/iofogctl_md/iofogctl_attach_exec.md @@ -1,15 +1,15 @@ ## iofogctl attach exec -Attach an Exec Session to a resource +Provision fog debug exec on an Agent ### Synopsis -Attach an Exec Session to a Microservice or Agent. +Provision fog debug exec resources. Use exec agent to open an interactive shell after provisioning. ### Examples ``` -iofogctl attach exec microservice AppName/MicroserviceName +iofogctl attach exec agent AgentName ``` ### Options @@ -29,7 +29,6 @@ iofogctl attach exec microservice AppName/MicroserviceName ### SEE ALSO * [iofogctl attach](iofogctl_attach.md) - Attach one ioFog resource to another -* [iofogctl attach exec agent](iofogctl_attach_exec_agent.md) - Attach an Exec Session to an Agent -* [iofogctl attach exec microservice](iofogctl_attach_exec_microservice.md) - Attach an Exec Session to a Microservice +* [iofogctl attach exec agent](iofogctl_attach_exec_agent.md) - Provision a fog debug exec microservice on an Agent diff --git a/docs/md/iofogctl_attach_exec_agent.md b/docs/iofogctl_md/iofogctl_attach_exec_agent.md similarity index 70% rename from docs/md/iofogctl_attach_exec_agent.md rename to docs/iofogctl_md/iofogctl_attach_exec_agent.md index df6d88801..e745247dc 100644 --- a/docs/md/iofogctl_attach_exec_agent.md +++ b/docs/iofogctl_md/iofogctl_attach_exec_agent.md @@ -1,10 +1,10 @@ ## iofogctl attach exec agent -Attach an Exec Session to an Agent +Provision a fog debug exec microservice on an Agent ### Synopsis -Attach an Exec Session to an existing Agent. +Provision a debug microservice on an Agent for interactive exec via POST /iofog/{uuid}/exec. ``` iofogctl attach exec agent NAME [DEBUG_IMAGE] [flags] @@ -32,6 +32,6 @@ iofogctl attach exec agent AgentName DebugImage ### SEE ALSO -* [iofogctl attach exec](iofogctl_attach_exec.md) - Attach an Exec Session to a resource +* [iofogctl attach exec](iofogctl_attach_exec.md) - Provision fog debug exec on an Agent diff --git a/docs/md/iofogctl_attach_volume-mount.md b/docs/iofogctl_md/iofogctl_attach_volume-mount.md similarity index 100% rename from docs/md/iofogctl_attach_volume-mount.md rename to docs/iofogctl_md/iofogctl_attach_volume-mount.md diff --git a/docs/md/iofogctl_completion.md b/docs/iofogctl_md/iofogctl_completion.md similarity index 100% rename from docs/md/iofogctl_completion.md rename to docs/iofogctl_md/iofogctl_completion.md diff --git a/docs/md/iofogctl_completion_bash.md b/docs/iofogctl_md/iofogctl_completion_bash.md similarity index 100% rename from docs/md/iofogctl_completion_bash.md rename to docs/iofogctl_md/iofogctl_completion_bash.md diff --git a/docs/md/iofogctl_completion_fish.md b/docs/iofogctl_md/iofogctl_completion_fish.md similarity index 100% rename from docs/md/iofogctl_completion_fish.md rename to docs/iofogctl_md/iofogctl_completion_fish.md diff --git a/docs/md/iofogctl_completion_powershell.md b/docs/iofogctl_md/iofogctl_completion_powershell.md similarity index 100% rename from docs/md/iofogctl_completion_powershell.md rename to docs/iofogctl_md/iofogctl_completion_powershell.md diff --git a/docs/md/iofogctl_completion_zsh.md b/docs/iofogctl_md/iofogctl_completion_zsh.md similarity index 100% rename from docs/md/iofogctl_completion_zsh.md rename to docs/iofogctl_md/iofogctl_completion_zsh.md diff --git a/docs/md/iofogctl_configure.md b/docs/iofogctl_md/iofogctl_configure.md similarity index 61% rename from docs/md/iofogctl_configure.md rename to docs/iofogctl_md/iofogctl_configure.md index 25eaf61c9..ce0b622ff 100644 --- a/docs/md/iofogctl_configure.md +++ b/docs/iofogctl_md/iofogctl_configure.md @@ -21,6 +21,7 @@ iofogctl configure controller NAME --user USER --key KEYFILE --port PORTNUM controllers agent agents + controlplane iofogctl configure controlplane --kube FILE ``` @@ -28,12 +29,14 @@ iofogctl configure controlplane --kube FILE ### Options ``` - --detached Specify command is to run against detached resources - -h, --help help for configure - --key string Path to private SSH key - --kube string Path to Kubernetes configuration file - --port int Port number that iofogctl uses to SSH into remote hosts - --user string Username of remote host + --ca string Path to PEM CA certificate for controller TLS (persisted to namespace config) + --ca-b64 string Base64-encoded PEM CA certificate for controller TLS (persisted to namespace config) + --detached Specify command is to run against detached resources + -h, --help help for configure + --key string Path to private SSH key + --kube string Path to Kubernetes configuration file + --port int Port number that iofogctl uses to SSH into remote hosts + --user string Username of remote host ``` ### Options inherited from parent commands diff --git a/docs/md/iofogctl_connect.md b/docs/iofogctl_md/iofogctl_connect.md similarity index 84% rename from docs/md/iofogctl_connect.md rename to docs/iofogctl_md/iofogctl_connect.md index 944cc9319..058fb6dab 100644 --- a/docs/md/iofogctl_connect.md +++ b/docs/iofogctl_md/iofogctl_connect.md @@ -8,7 +8,7 @@ Connect to an existing Control Plane. This command must be executed within an empty or non-existent Namespace. All resources provisioned with the corresponding Control Plane will become visible under the Namespace. -Visit iofog.org to view all YAML specifications usable with this command. +Visit https://iofog.org to view all YAML specifications usable with this command. ``` iofogctl connect [flags] @@ -29,6 +29,8 @@ iofogctl connect --generate ``` --b64 Indicate whether input password (--pass) is base64 encoded or not + --ca string Path to PEM CA certificate for controller TLS (persisted to namespace config) + --ca-b64 string Base64-encoded PEM CA certificate for controller TLS (persisted to namespace config) --ecn-addr string URL of Edge Compute Network to connect to --email string ioFog user email address -f, --file string YAML file containing specifications for ioFog resources to deploy diff --git a/docs/md/iofogctl_create.md b/docs/iofogctl_md/iofogctl_create.md similarity index 100% rename from docs/md/iofogctl_create.md rename to docs/iofogctl_md/iofogctl_create.md diff --git a/docs/md/iofogctl_create_namespace.md b/docs/iofogctl_md/iofogctl_create_namespace.md similarity index 100% rename from docs/md/iofogctl_create_namespace.md rename to docs/iofogctl_md/iofogctl_create_namespace.md diff --git a/docs/md/iofogctl_delete.md b/docs/iofogctl_md/iofogctl_delete.md similarity index 90% rename from docs/md/iofogctl_delete.md rename to docs/iofogctl_md/iofogctl_delete.md index 2dc215d57..fffebe853 100644 --- a/docs/md/iofogctl_delete.md +++ b/docs/iofogctl_md/iofogctl_delete.md @@ -13,8 +13,9 @@ iofogctl delete [flags] ### Options ``` - -f, --file string YAML file containing specifications for ioFog resources to deploy - -h, --help help for delete + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -f, --file string YAML file containing specifications for ioFog resources to deploy + -h, --help help for delete ``` ### Options inherited from parent commands @@ -36,7 +37,6 @@ iofogctl delete [flags] * [iofogctl delete certificate](iofogctl_delete_certificate.md) - Delete a Certificate * [iofogctl delete configmap](iofogctl_delete_configmap.md) - Delete a ConfigMap * [iofogctl delete controller](iofogctl_delete_controller.md) - Delete a Controller -* [iofogctl delete edge-resource](iofogctl_delete_edge-resource.md) - Delete an Edge Resource * [iofogctl delete microservice](iofogctl_delete_microservice.md) - Delete a Microservice * [iofogctl delete namespace](iofogctl_delete_namespace.md) - Delete a Namespace * [iofogctl delete nats-account-rule](iofogctl_delete_nats-account-rule.md) - Delete a NATS account rule diff --git a/docs/md/iofogctl_delete_agent.md b/docs/iofogctl_md/iofogctl_delete_agent.md similarity index 91% rename from docs/md/iofogctl_delete_agent.md rename to docs/iofogctl_md/iofogctl_delete_agent.md index cd519bd68..e9ef15af0 100644 --- a/docs/md/iofogctl_delete_agent.md +++ b/docs/iofogctl_md/iofogctl_delete_agent.md @@ -34,6 +34,7 @@ iofogctl delete agent NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_all.md b/docs/iofogctl_md/iofogctl_delete_all.md similarity index 91% rename from docs/md/iofogctl_delete_all.md rename to docs/iofogctl_md/iofogctl_delete_all.md index f2bc5166e..56cd78d2a 100644 --- a/docs/md/iofogctl_delete_all.md +++ b/docs/iofogctl_md/iofogctl_delete_all.md @@ -32,6 +32,7 @@ iofogctl delete all -n NAMESPACE ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_application-template.md b/docs/iofogctl_md/iofogctl_delete_application-template.md similarity index 88% rename from docs/md/iofogctl_delete_application-template.md rename to docs/iofogctl_md/iofogctl_delete_application-template.md index da2823b9a..3cb081a5e 100644 --- a/docs/md/iofogctl_delete_application-template.md +++ b/docs/iofogctl_md/iofogctl_delete_application-template.md @@ -26,6 +26,7 @@ iofogctl delete application-template NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_application.md b/docs/iofogctl_md/iofogctl_delete_application.md similarity index 88% rename from docs/md/iofogctl_delete_application.md rename to docs/iofogctl_md/iofogctl_delete_application.md index b8041961f..b69727723 100644 --- a/docs/md/iofogctl_delete_application.md +++ b/docs/iofogctl_md/iofogctl_delete_application.md @@ -26,6 +26,7 @@ iofogctl delete application NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_catalogitem.md b/docs/iofogctl_md/iofogctl_delete_catalogitem.md similarity index 88% rename from docs/md/iofogctl_delete_catalogitem.md rename to docs/iofogctl_md/iofogctl_delete_catalogitem.md index b145be699..039a4a570 100644 --- a/docs/md/iofogctl_delete_catalogitem.md +++ b/docs/iofogctl_md/iofogctl_delete_catalogitem.md @@ -26,6 +26,7 @@ iofogctl delete catalogitem NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_certificate.md b/docs/iofogctl_md/iofogctl_delete_certificate.md similarity index 88% rename from docs/md/iofogctl_delete_certificate.md rename to docs/iofogctl_md/iofogctl_delete_certificate.md index d86da773b..0a2c1c27c 100644 --- a/docs/md/iofogctl_delete_certificate.md +++ b/docs/iofogctl_md/iofogctl_delete_certificate.md @@ -26,6 +26,7 @@ iofogctl delete certificate NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_configmap.md b/docs/iofogctl_md/iofogctl_delete_configmap.md similarity index 88% rename from docs/md/iofogctl_delete_configmap.md rename to docs/iofogctl_md/iofogctl_delete_configmap.md index 0e2d1568c..55907cb80 100644 --- a/docs/md/iofogctl_delete_configmap.md +++ b/docs/iofogctl_md/iofogctl_delete_configmap.md @@ -26,6 +26,7 @@ iofogctl delete configmap NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_controller.md b/docs/iofogctl_md/iofogctl_delete_controller.md similarity index 87% rename from docs/md/iofogctl_delete_controller.md rename to docs/iofogctl_md/iofogctl_delete_controller.md index cda16d9bc..32bae8a7f 100644 --- a/docs/md/iofogctl_delete_controller.md +++ b/docs/iofogctl_md/iofogctl_delete_controller.md @@ -26,6 +26,7 @@ iofogctl delete controller NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_microservice.md b/docs/iofogctl_md/iofogctl_delete_microservice.md similarity index 88% rename from docs/md/iofogctl_delete_microservice.md rename to docs/iofogctl_md/iofogctl_delete_microservice.md index f105d744c..0882c6bfa 100644 --- a/docs/md/iofogctl_delete_microservice.md +++ b/docs/iofogctl_md/iofogctl_delete_microservice.md @@ -26,6 +26,7 @@ iofogctl delete microservice NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_namespace.md b/docs/iofogctl_md/iofogctl_delete_namespace.md similarity index 90% rename from docs/md/iofogctl_delete_namespace.md rename to docs/iofogctl_md/iofogctl_delete_namespace.md index 03962dbc3..67358b3bf 100644 --- a/docs/md/iofogctl_delete_namespace.md +++ b/docs/iofogctl_md/iofogctl_delete_namespace.md @@ -31,6 +31,7 @@ iofogctl delete namespace NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_nats-account-rule.md b/docs/iofogctl_md/iofogctl_delete_nats-account-rule.md similarity index 88% rename from docs/md/iofogctl_delete_nats-account-rule.md rename to docs/iofogctl_md/iofogctl_delete_nats-account-rule.md index 8ecc1707b..f106915fd 100644 --- a/docs/md/iofogctl_delete_nats-account-rule.md +++ b/docs/iofogctl_md/iofogctl_delete_nats-account-rule.md @@ -26,6 +26,7 @@ iofogctl delete nats-account-rule NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_nats-user-rule.md b/docs/iofogctl_md/iofogctl_delete_nats-user-rule.md similarity index 88% rename from docs/md/iofogctl_delete_nats-user-rule.md rename to docs/iofogctl_md/iofogctl_delete_nats-user-rule.md index 4a0a589bc..d913375bf 100644 --- a/docs/md/iofogctl_delete_nats-user-rule.md +++ b/docs/iofogctl_md/iofogctl_delete_nats-user-rule.md @@ -26,6 +26,7 @@ iofogctl delete nats-user-rule NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_registry.md b/docs/iofogctl_md/iofogctl_delete_registry.md similarity index 88% rename from docs/md/iofogctl_delete_registry.md rename to docs/iofogctl_md/iofogctl_delete_registry.md index 1378d6038..14b0bebcf 100644 --- a/docs/md/iofogctl_delete_registry.md +++ b/docs/iofogctl_md/iofogctl_delete_registry.md @@ -26,6 +26,7 @@ iofogctl delete registry ID ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_role.md b/docs/iofogctl_md/iofogctl_delete_role.md similarity index 87% rename from docs/md/iofogctl_delete_role.md rename to docs/iofogctl_md/iofogctl_delete_role.md index af8649196..063613afe 100644 --- a/docs/md/iofogctl_delete_role.md +++ b/docs/iofogctl_md/iofogctl_delete_role.md @@ -26,6 +26,7 @@ iofogctl delete role NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_rolebinding.md b/docs/iofogctl_md/iofogctl_delete_rolebinding.md similarity index 88% rename from docs/md/iofogctl_delete_rolebinding.md rename to docs/iofogctl_md/iofogctl_delete_rolebinding.md index 039254aa5..b9f112e2d 100644 --- a/docs/md/iofogctl_delete_rolebinding.md +++ b/docs/iofogctl_md/iofogctl_delete_rolebinding.md @@ -26,6 +26,7 @@ iofogctl delete rolebinding NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_secret.md b/docs/iofogctl_md/iofogctl_delete_secret.md similarity index 87% rename from docs/md/iofogctl_delete_secret.md rename to docs/iofogctl_md/iofogctl_delete_secret.md index f844d9f29..65d33f5ab 100644 --- a/docs/md/iofogctl_delete_secret.md +++ b/docs/iofogctl_md/iofogctl_delete_secret.md @@ -26,6 +26,7 @@ iofogctl delete secret NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_service.md b/docs/iofogctl_md/iofogctl_delete_service.md similarity index 88% rename from docs/md/iofogctl_delete_service.md rename to docs/iofogctl_md/iofogctl_delete_service.md index 422013a44..3da4bf1d7 100644 --- a/docs/md/iofogctl_delete_service.md +++ b/docs/iofogctl_md/iofogctl_delete_service.md @@ -26,6 +26,7 @@ iofogctl delete service NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_serviceaccount.md b/docs/iofogctl_md/iofogctl_delete_serviceaccount.md similarity index 90% rename from docs/md/iofogctl_delete_serviceaccount.md rename to docs/iofogctl_md/iofogctl_delete_serviceaccount.md index 5a32f7fba..c6e448117 100644 --- a/docs/md/iofogctl_delete_serviceaccount.md +++ b/docs/iofogctl_md/iofogctl_delete_serviceaccount.md @@ -26,6 +26,7 @@ iofogctl delete serviceaccount myapp/my-sa ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_volume-mount.md b/docs/iofogctl_md/iofogctl_delete_volume-mount.md similarity index 88% rename from docs/md/iofogctl_delete_volume-mount.md rename to docs/iofogctl_md/iofogctl_delete_volume-mount.md index fad5f3e47..54eadfa60 100644 --- a/docs/md/iofogctl_delete_volume-mount.md +++ b/docs/iofogctl_md/iofogctl_delete_volume-mount.md @@ -26,6 +26,7 @@ iofogctl delete volume-mount NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_delete_volume.md b/docs/iofogctl_md/iofogctl_delete_volume.md similarity index 88% rename from docs/md/iofogctl_delete_volume.md rename to docs/iofogctl_md/iofogctl_delete_volume.md index 834dfc03f..58444e05d 100644 --- a/docs/md/iofogctl_delete_volume.md +++ b/docs/iofogctl_md/iofogctl_delete_volume.md @@ -28,6 +28,7 @@ iofogctl delete volume NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") -v, --verbose Toggle for displaying verbose output of iofogctl ``` diff --git a/docs/md/iofogctl_deploy.md b/docs/iofogctl_md/iofogctl_deploy.md similarity index 90% rename from docs/md/iofogctl_deploy.md rename to docs/iofogctl_md/iofogctl_deploy.md index 077c88c10..3bff7c977 100644 --- a/docs/md/iofogctl_deploy.md +++ b/docs/iofogctl_md/iofogctl_deploy.md @@ -5,7 +5,7 @@ Deploy Edge Compute Network components on existing infrastructure ### Synopsis Deploy Edge Compute Network components on existing infrastructure. -Visit iofog.org to view all YAML specifications usable with this command. +Visit https://iofog.org to view all YAML specifications usable with this command. ``` iofogctl deploy [flags] @@ -14,11 +14,10 @@ iofogctl deploy [flags] ### Examples ``` -deploy -f ecn.yaml +iofogctl deploy -f ecn.yaml application-template.yaml application.yaml microservice.yaml - edge-resource.yaml catalog.yaml volume.yaml route.yaml diff --git a/docs/md/iofogctl_describe.md b/docs/iofogctl_md/iofogctl_describe.md similarity index 96% rename from docs/md/iofogctl_describe.md rename to docs/iofogctl_md/iofogctl_describe.md index a6439b0d7..14e6cbaa6 100644 --- a/docs/md/iofogctl_describe.md +++ b/docs/iofogctl_md/iofogctl_describe.md @@ -34,7 +34,6 @@ Most resources require a working Controller in the Namespace in order to be desc * [iofogctl describe configmap](iofogctl_describe_configmap.md) - Get detailed information about a ConfigMap * [iofogctl describe controller](iofogctl_describe_controller.md) - Get detailed information about a Controller * [iofogctl describe controlplane](iofogctl_describe_controlplane.md) - Get detailed information about a Control Plane -* [iofogctl describe edge-resource](iofogctl_describe_edge-resource.md) - Get detailed information about an Edge Resource * [iofogctl describe microservice](iofogctl_describe_microservice.md) - Get detailed information about a Microservice * [iofogctl describe namespace](iofogctl_describe_namespace.md) - Get detailed information about a Namespace * [iofogctl describe nats-account](iofogctl_describe_nats-account.md) - Get detailed information about a NATS account diff --git a/docs/md/iofogctl_describe_agent-config.md b/docs/iofogctl_md/iofogctl_describe_agent-config.md similarity index 100% rename from docs/md/iofogctl_describe_agent-config.md rename to docs/iofogctl_md/iofogctl_describe_agent-config.md diff --git a/docs/md/iofogctl_describe_agent.md b/docs/iofogctl_md/iofogctl_describe_agent.md similarity index 100% rename from docs/md/iofogctl_describe_agent.md rename to docs/iofogctl_md/iofogctl_describe_agent.md diff --git a/docs/md/iofogctl_describe_application-template.md b/docs/iofogctl_md/iofogctl_describe_application-template.md similarity index 100% rename from docs/md/iofogctl_describe_application-template.md rename to docs/iofogctl_md/iofogctl_describe_application-template.md diff --git a/docs/md/iofogctl_describe_application.md b/docs/iofogctl_md/iofogctl_describe_application.md similarity index 100% rename from docs/md/iofogctl_describe_application.md rename to docs/iofogctl_md/iofogctl_describe_application.md diff --git a/docs/md/iofogctl_describe_certificate.md b/docs/iofogctl_md/iofogctl_describe_certificate.md similarity index 100% rename from docs/md/iofogctl_describe_certificate.md rename to docs/iofogctl_md/iofogctl_describe_certificate.md diff --git a/docs/md/iofogctl_describe_configmap.md b/docs/iofogctl_md/iofogctl_describe_configmap.md similarity index 100% rename from docs/md/iofogctl_describe_configmap.md rename to docs/iofogctl_md/iofogctl_describe_configmap.md diff --git a/docs/md/iofogctl_describe_controller.md b/docs/iofogctl_md/iofogctl_describe_controller.md similarity index 100% rename from docs/md/iofogctl_describe_controller.md rename to docs/iofogctl_md/iofogctl_describe_controller.md diff --git a/docs/md/iofogctl_describe_controlplane.md b/docs/iofogctl_md/iofogctl_describe_controlplane.md similarity index 100% rename from docs/md/iofogctl_describe_controlplane.md rename to docs/iofogctl_md/iofogctl_describe_controlplane.md diff --git a/docs/md/iofogctl_describe_microservice.md b/docs/iofogctl_md/iofogctl_describe_microservice.md similarity index 100% rename from docs/md/iofogctl_describe_microservice.md rename to docs/iofogctl_md/iofogctl_describe_microservice.md diff --git a/docs/md/iofogctl_describe_namespace.md b/docs/iofogctl_md/iofogctl_describe_namespace.md similarity index 100% rename from docs/md/iofogctl_describe_namespace.md rename to docs/iofogctl_md/iofogctl_describe_namespace.md diff --git a/docs/md/iofogctl_describe_nats-account-rule.md b/docs/iofogctl_md/iofogctl_describe_nats-account-rule.md similarity index 100% rename from docs/md/iofogctl_describe_nats-account-rule.md rename to docs/iofogctl_md/iofogctl_describe_nats-account-rule.md diff --git a/docs/md/iofogctl_describe_nats-account.md b/docs/iofogctl_md/iofogctl_describe_nats-account.md similarity index 100% rename from docs/md/iofogctl_describe_nats-account.md rename to docs/iofogctl_md/iofogctl_describe_nats-account.md diff --git a/docs/md/iofogctl_describe_nats-user-rule.md b/docs/iofogctl_md/iofogctl_describe_nats-user-rule.md similarity index 100% rename from docs/md/iofogctl_describe_nats-user-rule.md rename to docs/iofogctl_md/iofogctl_describe_nats-user-rule.md diff --git a/docs/md/iofogctl_describe_nats-user.md b/docs/iofogctl_md/iofogctl_describe_nats-user.md similarity index 100% rename from docs/md/iofogctl_describe_nats-user.md rename to docs/iofogctl_md/iofogctl_describe_nats-user.md diff --git a/docs/md/iofogctl_describe_registry.md b/docs/iofogctl_md/iofogctl_describe_registry.md similarity index 100% rename from docs/md/iofogctl_describe_registry.md rename to docs/iofogctl_md/iofogctl_describe_registry.md diff --git a/docs/md/iofogctl_describe_role.md b/docs/iofogctl_md/iofogctl_describe_role.md similarity index 100% rename from docs/md/iofogctl_describe_role.md rename to docs/iofogctl_md/iofogctl_describe_role.md diff --git a/docs/md/iofogctl_describe_rolebinding.md b/docs/iofogctl_md/iofogctl_describe_rolebinding.md similarity index 100% rename from docs/md/iofogctl_describe_rolebinding.md rename to docs/iofogctl_md/iofogctl_describe_rolebinding.md diff --git a/docs/md/iofogctl_describe_secret.md b/docs/iofogctl_md/iofogctl_describe_secret.md similarity index 100% rename from docs/md/iofogctl_describe_secret.md rename to docs/iofogctl_md/iofogctl_describe_secret.md diff --git a/docs/md/iofogctl_describe_service.md b/docs/iofogctl_md/iofogctl_describe_service.md similarity index 100% rename from docs/md/iofogctl_describe_service.md rename to docs/iofogctl_md/iofogctl_describe_service.md diff --git a/docs/md/iofogctl_describe_serviceaccount.md b/docs/iofogctl_md/iofogctl_describe_serviceaccount.md similarity index 100% rename from docs/md/iofogctl_describe_serviceaccount.md rename to docs/iofogctl_md/iofogctl_describe_serviceaccount.md diff --git a/docs/md/iofogctl_describe_system-microservice.md b/docs/iofogctl_md/iofogctl_describe_system-microservice.md similarity index 100% rename from docs/md/iofogctl_describe_system-microservice.md rename to docs/iofogctl_md/iofogctl_describe_system-microservice.md diff --git a/docs/md/iofogctl_describe_volume-mount.md b/docs/iofogctl_md/iofogctl_describe_volume-mount.md similarity index 100% rename from docs/md/iofogctl_describe_volume-mount.md rename to docs/iofogctl_md/iofogctl_describe_volume-mount.md diff --git a/docs/md/iofogctl_describe_volume.md b/docs/iofogctl_md/iofogctl_describe_volume.md similarity index 100% rename from docs/md/iofogctl_describe_volume.md rename to docs/iofogctl_md/iofogctl_describe_volume.md diff --git a/docs/md/iofogctl_detach.md b/docs/iofogctl_md/iofogctl_detach.md similarity index 78% rename from docs/md/iofogctl_detach.md rename to docs/iofogctl_md/iofogctl_detach.md index 03b896633..0e367f78c 100644 --- a/docs/md/iofogctl_detach.md +++ b/docs/iofogctl_md/iofogctl_detach.md @@ -9,7 +9,7 @@ Detach one ioFog resource from another. ### Examples ``` -detach +iofogctl detach ``` ### Options @@ -30,8 +30,7 @@ detach * [iofogctl](iofogctl.md) - * [iofogctl detach agent](iofogctl_detach_agent.md) - Detaches an Agent -* [iofogctl detach edge-resource](iofogctl_detach_edge-resource.md) - Detaches an Edge Resource from an Agent -* [iofogctl detach exec](iofogctl_detach_exec.md) - Detach an Exec Session to a resource +* [iofogctl detach exec](iofogctl_detach_exec.md) - Remove fog debug exec from an Agent * [iofogctl detach volume-mount](iofogctl_detach_volume-mount.md) - Detach a Volume Mount from existing Agents diff --git a/docs/md/iofogctl_detach_agent.md b/docs/iofogctl_md/iofogctl_detach_agent.md similarity index 100% rename from docs/md/iofogctl_detach_agent.md rename to docs/iofogctl_md/iofogctl_detach_agent.md diff --git a/docs/md/iofogctl_detach_exec.md b/docs/iofogctl_md/iofogctl_detach_exec.md similarity index 66% rename from docs/md/iofogctl_detach_exec.md rename to docs/iofogctl_md/iofogctl_detach_exec.md index 6c2843108..61d6e8e9d 100644 --- a/docs/md/iofogctl_detach_exec.md +++ b/docs/iofogctl_md/iofogctl_detach_exec.md @@ -1,15 +1,15 @@ ## iofogctl detach exec -Detach an Exec Session to a resource +Remove fog debug exec from an Agent ### Synopsis -Detach an Exec Session to a Microservice or Agent. +Remove fog debug exec resources provisioned with attach exec agent. ### Examples ``` -iofogctl detach exec microservice AppName/MicroserviceName +iofogctl detach exec agent AgentName ``` ### Options @@ -29,7 +29,6 @@ iofogctl detach exec microservice AppName/MicroserviceName ### SEE ALSO * [iofogctl detach](iofogctl_detach.md) - Detach one ioFog resource from another -* [iofogctl detach exec agent](iofogctl_detach_exec_agent.md) - Detach an Exec Session from an Agent -* [iofogctl detach exec microservice](iofogctl_detach_exec_microservice.md) - Detach an Exec Session to a Microservice +* [iofogctl detach exec agent](iofogctl_detach_exec_agent.md) - Remove fog debug exec from an Agent diff --git a/docs/md/iofogctl_detach_exec_agent.md b/docs/iofogctl_md/iofogctl_detach_exec_agent.md similarity index 71% rename from docs/md/iofogctl_detach_exec_agent.md rename to docs/iofogctl_md/iofogctl_detach_exec_agent.md index 46af5f8ce..29a4dbcfc 100644 --- a/docs/md/iofogctl_detach_exec_agent.md +++ b/docs/iofogctl_md/iofogctl_detach_exec_agent.md @@ -1,10 +1,10 @@ ## iofogctl detach exec agent -Detach an Exec Session from an Agent +Remove fog debug exec from an Agent ### Synopsis -Detach an Exec Session from an existing Agent. +Remove the debug microservice provisioned for Agent exec via DELETE /iofog/{uuid}/exec. ``` iofogctl detach exec agent NAME [flags] @@ -32,6 +32,6 @@ iofogctl detach exec agent AgentName ### SEE ALSO -* [iofogctl detach exec](iofogctl_detach_exec.md) - Detach an Exec Session to a resource +* [iofogctl detach exec](iofogctl_detach_exec.md) - Remove fog debug exec from an Agent diff --git a/docs/md/iofogctl_detach_volume-mount.md b/docs/iofogctl_md/iofogctl_detach_volume-mount.md similarity index 100% rename from docs/md/iofogctl_detach_volume-mount.md rename to docs/iofogctl_md/iofogctl_detach_volume-mount.md diff --git a/docs/md/iofogctl_disconnect.md b/docs/iofogctl_md/iofogctl_disconnect.md similarity index 100% rename from docs/md/iofogctl_disconnect.md rename to docs/iofogctl_md/iofogctl_disconnect.md diff --git a/docs/md/iofogctl_exec.md b/docs/iofogctl_md/iofogctl_exec.md similarity index 79% rename from docs/md/iofogctl_exec.md rename to docs/iofogctl_md/iofogctl_exec.md index 1d8cc8ae4..3a0a304c1 100644 --- a/docs/md/iofogctl_exec.md +++ b/docs/iofogctl_md/iofogctl_exec.md @@ -23,7 +23,7 @@ Connect to an Exec Session of a Microservice or Agent. ### SEE ALSO * [iofogctl](iofogctl.md) - -* [iofogctl exec agent](iofogctl_exec_agent.md) - Connect to an Exec Session of an Agent -* [iofogctl exec microservice](iofogctl_exec_microservice.md) - Connect to an Exec Session of a Microservice +* [iofogctl exec agent](iofogctl_exec_agent.md) - Open an interactive exec session on an Agent debug shell +* [iofogctl exec microservice](iofogctl_exec_microservice.md) - Open an interactive exec session to a Microservice diff --git a/docs/md/iofogctl_exec_agent.md b/docs/iofogctl_md/iofogctl_exec_agent.md similarity index 65% rename from docs/md/iofogctl_exec_agent.md rename to docs/iofogctl_md/iofogctl_exec_agent.md index 216235e90..832517a4d 100644 --- a/docs/md/iofogctl_exec_agent.md +++ b/docs/iofogctl_md/iofogctl_exec_agent.md @@ -1,19 +1,20 @@ ## iofogctl exec agent -Connect to an Exec Session of an Agent +Open an interactive exec session on an Agent debug shell ### Synopsis -Connect to an Exec Session of an Agent to interact with its container. +Open a WebSocket exec session to the Agent debug microservice. Provisions fog debug exec automatically when it is not already enabled. ``` -iofogctl exec agent AgentName [flags] +iofogctl exec agent AgentName [DEBUG_IMAGE] [flags] ``` ### Examples ``` iofogctl exec agent AgentName +iofogctl exec agent AgentName ghcr.io/org/debug:latest ``` ### Options diff --git a/docs/md/iofogctl_exec_microservice.md b/docs/iofogctl_md/iofogctl_exec_microservice.md similarity index 82% rename from docs/md/iofogctl_exec_microservice.md rename to docs/iofogctl_md/iofogctl_exec_microservice.md index 92309f54d..f9b93e29f 100644 --- a/docs/md/iofogctl_exec_microservice.md +++ b/docs/iofogctl_md/iofogctl_exec_microservice.md @@ -1,10 +1,10 @@ ## iofogctl exec microservice -Connect to an Exec Session of a Microservice +Open an interactive exec session to a Microservice ### Synopsis -Connect to an Exec Session of a Microservice to interact with its container. +Open a WebSocket exec session to a running Microservice. No attach step is required. ``` iofogctl exec microservice AppName/MsvcName [flags] diff --git a/docs/md/iofogctl_get.md b/docs/iofogctl_md/iofogctl_get.md similarity index 98% rename from docs/md/iofogctl_get.md rename to docs/iofogctl_md/iofogctl_get.md index d36e8c25f..cbadbf5b6 100644 --- a/docs/md/iofogctl_get.md +++ b/docs/iofogctl_md/iofogctl_get.md @@ -19,7 +19,6 @@ iofogctl get all namespaces controllers agents - edge-resources application-templates applications system-applications diff --git a/docs/md/iofogctl_logs.md b/docs/iofogctl_md/iofogctl_logs.md similarity index 92% rename from docs/md/iofogctl_logs.md rename to docs/iofogctl_md/iofogctl_logs.md index b35e47370..96e799cf1 100644 --- a/docs/md/iofogctl_logs.md +++ b/docs/iofogctl_md/iofogctl_logs.md @@ -24,7 +24,7 @@ iofogctl logs controller NAME --follow Follow log output (default true) -h, --help help for logs --since string Start time in ISO 8601 format (e.g., 2024-01-01T00:00:00Z) - --tail int Number of lines to tail (range: 1-10000) (default 100) + --tail int Number of lines to tail (range: 1-5000) (default 100) --until string End time in ISO 8601 format (e.g., 2024-01-02T00:00:00Z) ``` diff --git a/docs/md/iofogctl_move.md b/docs/iofogctl_md/iofogctl_move.md similarity index 100% rename from docs/md/iofogctl_move.md rename to docs/iofogctl_md/iofogctl_move.md diff --git a/docs/md/iofogctl_move_agent.md b/docs/iofogctl_md/iofogctl_move_agent.md similarity index 100% rename from docs/md/iofogctl_move_agent.md rename to docs/iofogctl_md/iofogctl_move_agent.md diff --git a/docs/md/iofogctl_move_microservice.md b/docs/iofogctl_md/iofogctl_move_microservice.md similarity index 100% rename from docs/md/iofogctl_move_microservice.md rename to docs/iofogctl_md/iofogctl_move_microservice.md diff --git a/docs/md/iofogctl_nats.md b/docs/iofogctl_md/iofogctl_nats.md similarity index 100% rename from docs/md/iofogctl_nats.md rename to docs/iofogctl_md/iofogctl_nats.md diff --git a/docs/md/iofogctl_nats_accounts.md b/docs/iofogctl_md/iofogctl_nats_accounts.md similarity index 100% rename from docs/md/iofogctl_nats_accounts.md rename to docs/iofogctl_md/iofogctl_nats_accounts.md diff --git a/docs/md/iofogctl_nats_accounts_ensure.md b/docs/iofogctl_md/iofogctl_nats_accounts_ensure.md similarity index 100% rename from docs/md/iofogctl_nats_accounts_ensure.md rename to docs/iofogctl_md/iofogctl_nats_accounts_ensure.md diff --git a/docs/md/iofogctl_nats_operator.md b/docs/iofogctl_md/iofogctl_nats_operator.md similarity index 100% rename from docs/md/iofogctl_nats_operator.md rename to docs/iofogctl_md/iofogctl_nats_operator.md diff --git a/docs/md/iofogctl_nats_operator_describe.md b/docs/iofogctl_md/iofogctl_nats_operator_describe.md similarity index 100% rename from docs/md/iofogctl_nats_operator_describe.md rename to docs/iofogctl_md/iofogctl_nats_operator_describe.md diff --git a/docs/md/iofogctl_nats_users.md b/docs/iofogctl_md/iofogctl_nats_users.md similarity index 100% rename from docs/md/iofogctl_nats_users.md rename to docs/iofogctl_md/iofogctl_nats_users.md diff --git a/docs/md/iofogctl_nats_users_create-mqtt-bearer.md b/docs/iofogctl_md/iofogctl_nats_users_create-mqtt-bearer.md similarity index 100% rename from docs/md/iofogctl_nats_users_create-mqtt-bearer.md rename to docs/iofogctl_md/iofogctl_nats_users_create-mqtt-bearer.md diff --git a/docs/md/iofogctl_nats_users_create.md b/docs/iofogctl_md/iofogctl_nats_users_create.md similarity index 100% rename from docs/md/iofogctl_nats_users_create.md rename to docs/iofogctl_md/iofogctl_nats_users_create.md diff --git a/docs/md/iofogctl_nats_users_creds.md b/docs/iofogctl_md/iofogctl_nats_users_creds.md similarity index 100% rename from docs/md/iofogctl_nats_users_creds.md rename to docs/iofogctl_md/iofogctl_nats_users_creds.md diff --git a/docs/md/iofogctl_nats_users_delete-mqtt-bearer.md b/docs/iofogctl_md/iofogctl_nats_users_delete-mqtt-bearer.md similarity index 100% rename from docs/md/iofogctl_nats_users_delete-mqtt-bearer.md rename to docs/iofogctl_md/iofogctl_nats_users_delete-mqtt-bearer.md diff --git a/docs/md/iofogctl_nats_users_delete.md b/docs/iofogctl_md/iofogctl_nats_users_delete.md similarity index 100% rename from docs/md/iofogctl_nats_users_delete.md rename to docs/iofogctl_md/iofogctl_nats_users_delete.md diff --git a/docs/md/iofogctl_prune.md b/docs/iofogctl_md/iofogctl_prune.md similarity index 100% rename from docs/md/iofogctl_prune.md rename to docs/iofogctl_md/iofogctl_prune.md diff --git a/docs/md/iofogctl_prune_agent.md b/docs/iofogctl_md/iofogctl_prune_agent.md similarity index 100% rename from docs/md/iofogctl_prune_agent.md rename to docs/iofogctl_md/iofogctl_prune_agent.md diff --git a/docs/md/iofogctl_rebuild.md b/docs/iofogctl_md/iofogctl_rebuild.md similarity index 100% rename from docs/md/iofogctl_rebuild.md rename to docs/iofogctl_md/iofogctl_rebuild.md diff --git a/docs/md/iofogctl_rebuild_microservice.md b/docs/iofogctl_md/iofogctl_rebuild_microservice.md similarity index 100% rename from docs/md/iofogctl_rebuild_microservice.md rename to docs/iofogctl_md/iofogctl_rebuild_microservice.md diff --git a/docs/md/iofogctl_rebuild_system-microservice.md b/docs/iofogctl_md/iofogctl_rebuild_system-microservice.md similarity index 100% rename from docs/md/iofogctl_rebuild_system-microservice.md rename to docs/iofogctl_md/iofogctl_rebuild_system-microservice.md diff --git a/docs/iofogctl_md/iofogctl_reconcile.md b/docs/iofogctl_md/iofogctl_reconcile.md new file mode 100644 index 000000000..06b96a84c --- /dev/null +++ b/docs/iofogctl_md/iofogctl_reconcile.md @@ -0,0 +1,29 @@ +## iofogctl reconcile + +Retry async platform provisioning for an agent or service + +### Synopsis + +Enqueue a manual platform reconcile and wait for provisioning to complete. + +### Options + +``` + -h, --help help for reconcile +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl](iofogctl.md) - +* [iofogctl reconcile agent](iofogctl_reconcile_agent.md) - Reconcile fog router/NATS platform for an agent +* [iofogctl reconcile service](iofogctl_reconcile_service.md) - Reconcile service hub provisioning + + diff --git a/docs/iofogctl_md/iofogctl_reconcile_agent.md b/docs/iofogctl_md/iofogctl_reconcile_agent.md new file mode 100644 index 000000000..a8568668a --- /dev/null +++ b/docs/iofogctl_md/iofogctl_reconcile_agent.md @@ -0,0 +1,33 @@ +## iofogctl reconcile agent + +Reconcile fog router/NATS platform for an agent + +``` +iofogctl reconcile agent NAME [flags] +``` + +### Examples + +``` +iofogctl reconcile agent my-agent +``` + +### Options + +``` + -h, --help help for agent +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl reconcile](iofogctl_reconcile.md) - Retry async platform provisioning for an agent or service + + diff --git a/docs/iofogctl_md/iofogctl_reconcile_service.md b/docs/iofogctl_md/iofogctl_reconcile_service.md new file mode 100644 index 000000000..77d9dffb5 --- /dev/null +++ b/docs/iofogctl_md/iofogctl_reconcile_service.md @@ -0,0 +1,33 @@ +## iofogctl reconcile service + +Reconcile service hub provisioning + +``` +iofogctl reconcile service NAME [flags] +``` + +### Examples + +``` +iofogctl reconcile service my-service +``` + +### Options + +``` + -h, --help help for service +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl reconcile](iofogctl_reconcile.md) - Retry async platform provisioning for an agent or service + + diff --git a/docs/md/iofogctl_rollback.md b/docs/iofogctl_md/iofogctl_rollback.md similarity index 100% rename from docs/md/iofogctl_rollback.md rename to docs/iofogctl_md/iofogctl_rollback.md diff --git a/docs/md/iofogctl_start.md b/docs/iofogctl_md/iofogctl_start.md similarity index 100% rename from docs/md/iofogctl_start.md rename to docs/iofogctl_md/iofogctl_start.md diff --git a/docs/md/iofogctl_start_application.md b/docs/iofogctl_md/iofogctl_start_application.md similarity index 100% rename from docs/md/iofogctl_start_application.md rename to docs/iofogctl_md/iofogctl_start_application.md diff --git a/docs/md/iofogctl_start_microservice.md b/docs/iofogctl_md/iofogctl_start_microservice.md similarity index 100% rename from docs/md/iofogctl_start_microservice.md rename to docs/iofogctl_md/iofogctl_start_microservice.md diff --git a/docs/md/iofogctl_stop.md b/docs/iofogctl_md/iofogctl_stop.md similarity index 100% rename from docs/md/iofogctl_stop.md rename to docs/iofogctl_md/iofogctl_stop.md diff --git a/docs/md/iofogctl_stop_application.md b/docs/iofogctl_md/iofogctl_stop_application.md similarity index 100% rename from docs/md/iofogctl_stop_application.md rename to docs/iofogctl_md/iofogctl_stop_application.md diff --git a/docs/md/iofogctl_stop_microservice.md b/docs/iofogctl_md/iofogctl_stop_microservice.md similarity index 100% rename from docs/md/iofogctl_stop_microservice.md rename to docs/iofogctl_md/iofogctl_stop_microservice.md diff --git a/docs/md/iofogctl_upgrade.md b/docs/iofogctl_md/iofogctl_upgrade.md similarity index 100% rename from docs/md/iofogctl_upgrade.md rename to docs/iofogctl_md/iofogctl_upgrade.md diff --git a/docs/md/iofogctl_version.md b/docs/iofogctl_md/iofogctl_version.md similarity index 100% rename from docs/md/iofogctl_version.md rename to docs/iofogctl_md/iofogctl_version.md diff --git a/docs/md/iofogctl_view.md b/docs/iofogctl_md/iofogctl_view.md similarity index 95% rename from docs/md/iofogctl_view.md rename to docs/iofogctl_md/iofogctl_view.md index 3cbf27c2c..1b14b1302 100644 --- a/docs/md/iofogctl_view.md +++ b/docs/iofogctl_md/iofogctl_view.md @@ -1,6 +1,6 @@ ## iofogctl view -Open ECN Viewer +Open EdgeOps Console ``` iofogctl view [flags] diff --git a/docs/md/iofogctl_delete_edge-resource.md b/docs/md/iofogctl_delete_edge-resource.md deleted file mode 100644 index 8cdd0f949..000000000 --- a/docs/md/iofogctl_delete_edge-resource.md +++ /dev/null @@ -1,40 +0,0 @@ -## iofogctl delete edge-resource - -Delete an Edge Resource - -### Synopsis - -Delete an Edge Resource. - -Only the specified version will be deleted. -Agents that this Edge Resource are attached to will be notified of the deletion. - -``` -iofogctl delete edge-resource NAME VERSION [flags] -``` - -### Examples - -``` -iofogctl delete edge-resource NAME VERSION -``` - -### Options - -``` - -h, --help help for edge-resource -``` - -### Options inherited from parent commands - -``` - --debug Toggle for displaying verbose output of API clients (HTTP and SSH) - -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl -``` - -### SEE ALSO - -* [iofogctl delete](iofogctl_delete.md) - Delete an existing ioFog resource - - diff --git a/docs/md/iofogctl_legacy.md b/docs/md/iofogctl_legacy.md deleted file mode 100644 index e39630ea1..000000000 --- a/docs/md/iofogctl_legacy.md +++ /dev/null @@ -1,43 +0,0 @@ -## iofogctl legacy - -Execute commands using legacy CLI - -### Synopsis - -Execute commands using legacy Controller and Agent CLI. - -Legacy commands require SSH access to the corresponding Agent or Controller. - -Use the configure command to add SSH details to Agents and Controllers if necessary. - -``` -iofogctl legacy resource NAME COMMAND ARGS... [flags] -``` - -### Examples - -``` -iofogctl legacy controller NAME COMMAND -iofogctl legacy agent NAME COMMAND -``` - -### Options - -``` - --detached Specify command is to run against detached resources - -h, --help help for legacy -``` - -### Options inherited from parent commands - -``` - --debug Toggle for displaying verbose output of API clients (HTTP and SSH) - -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl -``` - -### SEE ALSO - -* [iofogctl](iofogctl.md) - - - diff --git a/docs/md/iofogctl_rename.md b/docs/md/iofogctl_rename.md deleted file mode 100644 index 875fa18c2..000000000 --- a/docs/md/iofogctl_rename.md +++ /dev/null @@ -1,33 +0,0 @@ -## iofogctl rename - -Rename the iofog resources that are currently deployed - -### Synopsis - -Rename the iofog resources that are currently deployed - -### Options - -``` - -h, --help help for rename -``` - -### Options inherited from parent commands - -``` - --debug Toggle for displaying verbose output of API clients (HTTP and SSH) - -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl -``` - -### SEE ALSO - -* [iofogctl](iofogctl.md) - -* [iofogctl rename agent](iofogctl_rename_agent.md) - Rename an Agent -* [iofogctl rename application](iofogctl_rename_application.md) - Rename an Application -* [iofogctl rename controller](iofogctl_rename_controller.md) - Rename a Controller -* [iofogctl rename edge-resource](iofogctl_rename_edge-resource.md) - Rename an Edge Resource -* [iofogctl rename microservice](iofogctl_rename_microservice.md) - Rename a Microservice -* [iofogctl rename namespace](iofogctl_rename_namespace.md) - Rename a Namespace - - diff --git a/docs/md/iofogctl_rename_edge-resource.md b/docs/md/iofogctl_rename_edge-resource.md deleted file mode 100644 index f1b70d666..000000000 --- a/docs/md/iofogctl_rename_edge-resource.md +++ /dev/null @@ -1,37 +0,0 @@ -## iofogctl rename edge-resource - -Rename an Edge Resource - -### Synopsis - -Rename an Edge Resource - -``` -iofogctl rename edge-resource NAME NEW_NAME [flags] -``` - -### Examples - -``` -iofogctl rename edge-resource NAME NEW_NAME -``` - -### Options - -``` - -h, --help help for edge-resource -``` - -### Options inherited from parent commands - -``` - --debug Toggle for displaying verbose output of API clients (HTTP and SSH) - -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl -``` - -### SEE ALSO - -* [iofogctl rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed - - diff --git a/docs/potctl_md/potctl.md b/docs/potctl_md/potctl.md new file mode 100644 index 000000000..d5f5f326f --- /dev/null +++ b/docs/potctl_md/potctl.md @@ -0,0 +1,45 @@ +## potctl + + + +``` +potctl [flags] +``` + +### Options + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -h, --help help for potctl + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl attach](potctl_attach.md) - Attach one ioFog resource to another +* [potctl completion](potctl_completion.md) - Generate the autocompletion script for the specified shell +* [potctl configure](potctl_configure.md) - Configure potctl or ioFog resources +* [potctl connect](potctl_connect.md) - Connect to an existing Control Plane +* [potctl create](potctl_create.md) - Create a resource +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource +* [potctl deploy](potctl_deploy.md) - Deploy Edge Compute Network components on existing infrastructure +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources +* [potctl detach](potctl_detach.md) - Detach one ioFog resource from another +* [potctl disconnect](potctl_disconnect.md) - Disconnect from an ioFog cluster +* [potctl exec](potctl_exec.md) - Connect to an Exec Session of a resource +* [potctl get](potctl_get.md) - Get information of existing resources +* [potctl logs](potctl_logs.md) - Get log contents of deployed resource +* [potctl move](potctl_move.md) - Move an existing resources inside the current Namespace +* [potctl nats](potctl_nats.md) - Manage NATS resources +* [potctl prune](potctl_prune.md) - prune ioFog resources +* [potctl rebuild](potctl_rebuild.md) - Rebuilds a microservice or system-microservice +* [potctl reconcile](potctl_reconcile.md) - Retry async platform provisioning for an agent or service +* [potctl rollback](potctl_rollback.md) - Rollback ioFog resources +* [potctl start](potctl_start.md) - Starts a resource +* [potctl stop](potctl_stop.md) - Stops a resource +* [potctl upgrade](potctl_upgrade.md) - Upgrade ioFog resources +* [potctl version](potctl_version.md) - Get CLI application version +* [potctl view](potctl_view.md) - Open EdgeOps Console + + diff --git a/docs/potctl_md/potctl_attach.md b/docs/potctl_md/potctl_attach.md new file mode 100644 index 000000000..3dc8ad7dd --- /dev/null +++ b/docs/potctl_md/potctl_attach.md @@ -0,0 +1,36 @@ +## potctl attach + +Attach one ioFog resource to another + +### Synopsis + +Attach one ioFog resource to another. + +### Examples + +``` +potctl attach +``` + +### Options + +``` + -h, --help help for attach +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl attach agent](potctl_attach_agent.md) - Attach an Agent to an existing Namespace +* [potctl attach exec](potctl_attach_exec.md) - Provision fog debug exec on an Agent +* [potctl attach volume-mount](potctl_attach_volume-mount.md) - Attach a Volume Mount to existing Agents + + diff --git a/docs/potctl_md/potctl_attach_agent.md b/docs/potctl_md/potctl_attach_agent.md new file mode 100644 index 000000000..76a935999 --- /dev/null +++ b/docs/potctl_md/potctl_attach_agent.md @@ -0,0 +1,39 @@ +## potctl attach agent + +Attach an Agent to an existing Namespace + +### Synopsis + +Attach a detached Agent to an existing Namespace. + +The Agent will be provisioned with the Controller within the Namespace. + +``` +potctl attach agent NAME [flags] +``` + +### Examples + +``` +potctl attach agent NAME +``` + +### Options + +``` + -h, --help help for agent +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl attach](potctl_attach.md) - Attach one ioFog resource to another + + diff --git a/docs/potctl_md/potctl_attach_exec.md b/docs/potctl_md/potctl_attach_exec.md new file mode 100644 index 000000000..358316a1b --- /dev/null +++ b/docs/potctl_md/potctl_attach_exec.md @@ -0,0 +1,34 @@ +## potctl attach exec + +Provision fog debug exec on an Agent + +### Synopsis + +Provision fog debug exec resources. Use exec agent to open an interactive shell after provisioning. + +### Examples + +``` +potctl attach exec agent AgentName +``` + +### Options + +``` + -h, --help help for exec +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl attach](potctl_attach.md) - Attach one ioFog resource to another +* [potctl attach exec agent](potctl_attach_exec_agent.md) - Provision a fog debug exec microservice on an Agent + + diff --git a/docs/potctl_md/potctl_attach_exec_agent.md b/docs/potctl_md/potctl_attach_exec_agent.md new file mode 100644 index 000000000..6fdfdef6a --- /dev/null +++ b/docs/potctl_md/potctl_attach_exec_agent.md @@ -0,0 +1,37 @@ +## potctl attach exec agent + +Provision a fog debug exec microservice on an Agent + +### Synopsis + +Provision a debug microservice on an Agent for interactive exec via POST /iofog/{uuid}/exec. + +``` +potctl attach exec agent NAME [DEBUG_IMAGE] [flags] +``` + +### Examples + +``` +potctl attach exec agent AgentName DebugImage +``` + +### Options + +``` + -h, --help help for agent +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl attach exec](potctl_attach_exec.md) - Provision fog debug exec on an Agent + + diff --git a/docs/potctl_md/potctl_attach_volume-mount.md b/docs/potctl_md/potctl_attach_volume-mount.md new file mode 100644 index 000000000..65e2fd60d --- /dev/null +++ b/docs/potctl_md/potctl_attach_volume-mount.md @@ -0,0 +1,37 @@ +## potctl attach volume-mount + +Attach a Volume Mount to existing Agents + +### Synopsis + +Attach a Volume Mount to existing Agents. + +``` +potctl attach volume-mount NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +potctl attach volume-mount NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for volume-mount +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl attach](potctl_attach.md) - Attach one ioFog resource to another + + diff --git a/docs/potctl_md/potctl_completion.md b/docs/potctl_md/potctl_completion.md new file mode 100644 index 000000000..68f34b600 --- /dev/null +++ b/docs/potctl_md/potctl_completion.md @@ -0,0 +1,33 @@ +## potctl completion + +Generate the autocompletion script for the specified shell + +### Synopsis + +Generate the autocompletion script for potctl for the specified shell. +See each sub-command's help for details on how to use the generated script. + + +### Options + +``` + -h, --help help for completion +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl completion bash](potctl_completion_bash.md) - Generate the autocompletion script for bash +* [potctl completion fish](potctl_completion_fish.md) - Generate the autocompletion script for fish +* [potctl completion powershell](potctl_completion_powershell.md) - Generate the autocompletion script for powershell +* [potctl completion zsh](potctl_completion_zsh.md) - Generate the autocompletion script for zsh + + diff --git a/docs/potctl_md/potctl_completion_bash.md b/docs/potctl_md/potctl_completion_bash.md new file mode 100644 index 000000000..a3dd2b979 --- /dev/null +++ b/docs/potctl_md/potctl_completion_bash.md @@ -0,0 +1,52 @@ +## potctl completion bash + +Generate the autocompletion script for bash + +### Synopsis + +Generate the autocompletion script for the bash shell. + +This script depends on the 'bash-completion' package. +If it is not installed already, you can install it via your OS's package manager. + +To load completions in your current shell session: + + source <(potctl completion bash) + +To load completions for every new session, execute once: + +#### Linux: + + potctl completion bash > /etc/bash_completion.d/potctl + +#### macOS: + + potctl completion bash > $(brew --prefix)/etc/bash_completion.d/potctl + +You will need to start a new shell for this setup to take effect. + + +``` +potctl completion bash +``` + +### Options + +``` + -h, --help help for bash + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl completion](potctl_completion.md) - Generate the autocompletion script for the specified shell + + diff --git a/docs/potctl_md/potctl_completion_fish.md b/docs/potctl_md/potctl_completion_fish.md new file mode 100644 index 000000000..4c752df8f --- /dev/null +++ b/docs/potctl_md/potctl_completion_fish.md @@ -0,0 +1,43 @@ +## potctl completion fish + +Generate the autocompletion script for fish + +### Synopsis + +Generate the autocompletion script for the fish shell. + +To load completions in your current shell session: + + potctl completion fish | source + +To load completions for every new session, execute once: + + potctl completion fish > ~/.config/fish/completions/potctl.fish + +You will need to start a new shell for this setup to take effect. + + +``` +potctl completion fish [flags] +``` + +### Options + +``` + -h, --help help for fish + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl completion](potctl_completion.md) - Generate the autocompletion script for the specified shell + + diff --git a/docs/potctl_md/potctl_completion_powershell.md b/docs/potctl_md/potctl_completion_powershell.md new file mode 100644 index 000000000..f290435e2 --- /dev/null +++ b/docs/potctl_md/potctl_completion_powershell.md @@ -0,0 +1,40 @@ +## potctl completion powershell + +Generate the autocompletion script for powershell + +### Synopsis + +Generate the autocompletion script for powershell. + +To load completions in your current shell session: + + potctl completion powershell | Out-String | Invoke-Expression + +To load completions for every new session, add the output of the above command +to your powershell profile. + + +``` +potctl completion powershell [flags] +``` + +### Options + +``` + -h, --help help for powershell + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl completion](potctl_completion.md) - Generate the autocompletion script for the specified shell + + diff --git a/docs/potctl_md/potctl_completion_zsh.md b/docs/potctl_md/potctl_completion_zsh.md new file mode 100644 index 000000000..d715317b6 --- /dev/null +++ b/docs/potctl_md/potctl_completion_zsh.md @@ -0,0 +1,54 @@ +## potctl completion zsh + +Generate the autocompletion script for zsh + +### Synopsis + +Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(potctl completion zsh) + +To load completions for every new session, execute once: + +#### Linux: + + potctl completion zsh > "${fpath[1]}/_potctl" + +#### macOS: + + potctl completion zsh > $(brew --prefix)/share/zsh/site-functions/_potctl + +You will need to start a new shell for this setup to take effect. + + +``` +potctl completion zsh [flags] +``` + +### Options + +``` + -h, --help help for zsh + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl completion](potctl_completion.md) - Generate the autocompletion script for the specified shell + + diff --git a/docs/potctl_md/potctl_configure.md b/docs/potctl_md/potctl_configure.md new file mode 100644 index 000000000..d791919ab --- /dev/null +++ b/docs/potctl_md/potctl_configure.md @@ -0,0 +1,54 @@ +## potctl configure + +Configure potctl or ioFog resources + +### Synopsis + +Configure potctl or ioFog resources + +If you would like to replace the host value of Remote Controllers or Agents, you should delete and redeploy those resources. + +``` +potctl configure RESOURCE NAME [flags] +``` + +### Examples + +``` +potctl configure current-namespace NAME + +potctl configure controller NAME --user USER --key KEYFILE --port PORTNUM + controllers + agent + agents + controlplane + +potctl configure controlplane --kube FILE +``` + +### Options + +``` + --ca string Path to PEM CA certificate for controller TLS (persisted to namespace config) + --ca-b64 string Base64-encoded PEM CA certificate for controller TLS (persisted to namespace config) + --detached Specify command is to run against detached resources + -h, --help help for configure + --key string Path to private SSH key + --kube string Path to Kubernetes configuration file + --port int Port number that potctl uses to SSH into remote hosts + --user string Username of remote host +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - + + diff --git a/docs/potctl_md/potctl_connect.md b/docs/potctl_md/potctl_connect.md new file mode 100644 index 000000000..241a411f6 --- /dev/null +++ b/docs/potctl_md/potctl_connect.md @@ -0,0 +1,57 @@ +## potctl connect + +Connect to an existing Control Plane + +### Synopsis + +Connect to an existing Control Plane. + +This command must be executed within an empty or non-existent Namespace. +All resources provisioned with the corresponding Control Plane will become visible under the Namespace. +Visit https://docs.datasance.com to view all YAML specifications usable with this command. + +``` +potctl connect [flags] +``` + +### Examples + +``` +potctl connect -f controlplane.yaml + +potctl connect --email EMAIL --pass PASSWORD --kube FILE + --email EMAIL --pass PASSWORD --ecn-addr ENDPOINT --name NAME + +potctl connect --generate +``` + +### Options + +``` + --b64 Indicate whether input password (--pass) is base64 encoded or not + --ca string Path to PEM CA certificate for controller TLS (persisted to namespace config) + --ca-b64 string Base64-encoded PEM CA certificate for controller TLS (persisted to namespace config) + --ecn-addr string URL of Edge Compute Network to connect to + --email string ioFog user email address + -f, --file string YAML file containing specifications for ioFog resources to deploy + --force Overwrite existing Namespace + --generate Generate a connection string that can be used to connect to this ECN + -h, --help help for connect + --kube string Kubernetes config file. Typically ~/.kube/config + --name string Name you would like to assign to Controller + --pass string ioFog user password +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - + + diff --git a/docs/potctl_md/potctl_create.md b/docs/potctl_md/potctl_create.md new file mode 100644 index 000000000..3953f3596 --- /dev/null +++ b/docs/potctl_md/potctl_create.md @@ -0,0 +1,28 @@ +## potctl create + +Create a resource + +### Synopsis + +Create a component of an Edge Compute Network. + +### Options + +``` + -h, --help help for create +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl create namespace](potctl_create_namespace.md) - Create a Namespace + + diff --git a/docs/potctl_md/potctl_create_namespace.md b/docs/potctl_md/potctl_create_namespace.md new file mode 100644 index 000000000..0d789e1d9 --- /dev/null +++ b/docs/potctl_md/potctl_create_namespace.md @@ -0,0 +1,41 @@ +## potctl create namespace + +Create a Namespace + +### Synopsis + +Create a Namespace. + +A Namespace contains all components of an Edge Compute Network. + +A single instance of potctl can be used to manage any number of Edge Compute Networks. + +``` +potctl create namespace NAME [flags] +``` + +### Examples + +``` +potctl create namespace NAME +``` + +### Options + +``` + -h, --help help for namespace +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl create](potctl_create.md) - Create a resource + + diff --git a/docs/potctl_md/potctl_delete.md b/docs/potctl_md/potctl_delete.md new file mode 100644 index 000000000..1d839e20b --- /dev/null +++ b/docs/potctl_md/potctl_delete.md @@ -0,0 +1,53 @@ +## potctl delete + +Delete an existing ioFog resource + +### Synopsis + +Delete an existing ioFog resource. + +``` +potctl delete [flags] +``` + +### Options + +``` + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -f, --file string YAML file containing specifications for ioFog resources to deploy + -h, --help help for delete +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl delete agent](potctl_delete_agent.md) - Delete an Agent +* [potctl delete all](potctl_delete_all.md) - Delete all resources within a namespace +* [potctl delete application](potctl_delete_application.md) - Delete an application +* [potctl delete application-template](potctl_delete_application-template.md) - Delete an application-template +* [potctl delete catalogitem](potctl_delete_catalogitem.md) - Delete a Catalog item +* [potctl delete certificate](potctl_delete_certificate.md) - Delete a Certificate +* [potctl delete configmap](potctl_delete_configmap.md) - Delete a ConfigMap +* [potctl delete controller](potctl_delete_controller.md) - Delete a Controller +* [potctl delete microservice](potctl_delete_microservice.md) - Delete a Microservice +* [potctl delete namespace](potctl_delete_namespace.md) - Delete a Namespace +* [potctl delete nats-account-rule](potctl_delete_nats-account-rule.md) - Delete a NATS account rule +* [potctl delete nats-user-rule](potctl_delete_nats-user-rule.md) - Delete a NATS user rule +* [potctl delete registry](potctl_delete_registry.md) - Delete a Registry +* [potctl delete role](potctl_delete_role.md) - Delete a Role +* [potctl delete rolebinding](potctl_delete_rolebinding.md) - Delete a RoleBinding +* [potctl delete secret](potctl_delete_secret.md) - Delete a Secret +* [potctl delete service](potctl_delete_service.md) - Delete a Service +* [potctl delete serviceaccount](potctl_delete_serviceaccount.md) - Delete a ServiceAccount +* [potctl delete volume](potctl_delete_volume.md) - Delete an Volume +* [potctl delete volume-mount](potctl_delete_volume-mount.md) - Delete a Volume Mount + + diff --git a/docs/potctl_md/potctl_delete_agent.md b/docs/potctl_md/potctl_delete_agent.md new file mode 100644 index 000000000..72f0e48e0 --- /dev/null +++ b/docs/potctl_md/potctl_delete_agent.md @@ -0,0 +1,46 @@ +## potctl delete agent + +Delete an Agent + +### Synopsis + +Delete an Agent. + +The Agent will be unprovisioned from the Controller within the namespace. + +The Agent stack will be uninstalled from the host. + +If you wish to not remove the Agent stack from the host, please use potctl detach agent + +``` +potctl delete agent NAME [flags] +``` + +### Examples + +``` +potctl delete agent NAME +``` + +### Options + +``` + --detached Specify command is to run against detached resources + --force Remove even if there are still Microservices running on the Agent + -h, --help help for agent +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_all.md b/docs/potctl_md/potctl_delete_all.md new file mode 100644 index 000000000..54d776314 --- /dev/null +++ b/docs/potctl_md/potctl_delete_all.md @@ -0,0 +1,44 @@ +## potctl delete all + +Delete all resources within a namespace + +### Synopsis + +Delete all resources within a namespace. + +Tears down all components of an Edge Compute Network. + +If you don't want to tear down the deployments but would like to free up the Namespace, use the disconnect command instead. + +``` +potctl delete all [flags] +``` + +### Examples + +``` +potctl delete all -n NAMESPACE +``` + +### Options + +``` + --detached Specify command is to run against detached resources + --force Force deletion of Agents + -h, --help help for all +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_application-template.md b/docs/potctl_md/potctl_delete_application-template.md new file mode 100644 index 000000000..6e1dfbdb3 --- /dev/null +++ b/docs/potctl_md/potctl_delete_application-template.md @@ -0,0 +1,38 @@ +## potctl delete application-template + +Delete an application-template + +### Synopsis + +Delete an application-template + +``` +potctl delete application-template NAME [flags] +``` + +### Examples + +``` +potctl delete application-template NAME +``` + +### Options + +``` + -h, --help help for application-template +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_application.md b/docs/potctl_md/potctl_delete_application.md new file mode 100644 index 000000000..9b1bb347c --- /dev/null +++ b/docs/potctl_md/potctl_delete_application.md @@ -0,0 +1,38 @@ +## potctl delete application + +Delete an application + +### Synopsis + +Delete an application and all its components + +``` +potctl delete application NAME [flags] +``` + +### Examples + +``` +potctl delete application NAME +``` + +### Options + +``` + -h, --help help for application +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_catalogitem.md b/docs/potctl_md/potctl_delete_catalogitem.md new file mode 100644 index 000000000..328c93d69 --- /dev/null +++ b/docs/potctl_md/potctl_delete_catalogitem.md @@ -0,0 +1,38 @@ +## potctl delete catalogitem + +Delete a Catalog item + +### Synopsis + +Delete a Catalog item from the Controller. + +``` +potctl delete catalogitem NAME [flags] +``` + +### Examples + +``` +potctl delete catalogitem NAME +``` + +### Options + +``` + -h, --help help for catalogitem +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_certificate.md b/docs/potctl_md/potctl_delete_certificate.md new file mode 100644 index 000000000..081884180 --- /dev/null +++ b/docs/potctl_md/potctl_delete_certificate.md @@ -0,0 +1,38 @@ +## potctl delete certificate + +Delete a Certificate + +### Synopsis + +Delete a Certificate from the Controller. + +``` +potctl delete certificate NAME [flags] +``` + +### Examples + +``` +potctl delete certificate NAME +``` + +### Options + +``` + -h, --help help for certificate +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_configmap.md b/docs/potctl_md/potctl_delete_configmap.md new file mode 100644 index 000000000..a0e8851d4 --- /dev/null +++ b/docs/potctl_md/potctl_delete_configmap.md @@ -0,0 +1,38 @@ +## potctl delete configmap + +Delete a ConfigMap + +### Synopsis + +Delete a ConfigMap from the Controller. + +``` +potctl delete configmap NAME [flags] +``` + +### Examples + +``` +potctl delete configmap NAME +``` + +### Options + +``` + -h, --help help for configmap +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/md/iofogctl_rename_controller.md b/docs/potctl_md/potctl_delete_controller.md similarity index 57% rename from docs/md/iofogctl_rename_controller.md rename to docs/potctl_md/potctl_delete_controller.md index e262c6b02..4b50fcacc 100644 --- a/docs/md/iofogctl_rename_controller.md +++ b/docs/potctl_md/potctl_delete_controller.md @@ -1,19 +1,19 @@ -## iofogctl rename controller +## potctl delete controller -Rename a Controller +Delete a Controller ### Synopsis -Rename a Controller +Delete a Controller. ``` -iofogctl rename controller NAME NEW_NAME [flags] +potctl delete controller NAME [flags] ``` ### Examples ``` -iofogctl rename controller NAME NEW_NAME +potctl delete controller NAME ``` ### Options @@ -26,12 +26,13 @@ iofogctl rename controller NAME NEW_NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl + -v, --verbose Toggle for displaying verbose output of potctl ``` ### SEE ALSO -* [iofogctl rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource diff --git a/docs/md/iofogctl_attach_exec_microservice.md b/docs/potctl_md/potctl_delete_microservice.md similarity index 55% rename from docs/md/iofogctl_attach_exec_microservice.md rename to docs/potctl_md/potctl_delete_microservice.md index 908d81b3e..744dcbd33 100644 --- a/docs/md/iofogctl_attach_exec_microservice.md +++ b/docs/potctl_md/potctl_delete_microservice.md @@ -1,19 +1,19 @@ -## iofogctl attach exec microservice +## potctl delete microservice -Attach an Exec Session to a Microservice +Delete a Microservice ### Synopsis -Attach an Exec Session to an existing Microservice. +Delete a Microservice ``` -iofogctl attach exec microservice NAME [flags] +potctl delete microservice NAME [flags] ``` ### Examples ``` -iofogctl attach exec microservice AppName/MicroserviceName +potctl delete microservice NAME ``` ### Options @@ -26,12 +26,13 @@ iofogctl attach exec microservice AppName/MicroserviceName ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl + -v, --verbose Toggle for displaying verbose output of potctl ``` ### SEE ALSO -* [iofogctl attach exec](iofogctl_attach_exec.md) - Attach an Exec Session to a resource +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource diff --git a/docs/potctl_md/potctl_delete_namespace.md b/docs/potctl_md/potctl_delete_namespace.md new file mode 100644 index 000000000..a7d48150a --- /dev/null +++ b/docs/potctl_md/potctl_delete_namespace.md @@ -0,0 +1,43 @@ +## potctl delete namespace + +Delete a Namespace + +### Synopsis + +Delete a Namespace. + +The Namespace must be empty. + +If you would like to delete all resources in the Namespace, use the --force flag. + +``` +potctl delete namespace NAME [flags] +``` + +### Examples + +``` +potctl delete namespace NAME +``` + +### Options + +``` + --force Force deletion of all resources within the Namespace + -h, --help help for namespace +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_nats-account-rule.md b/docs/potctl_md/potctl_delete_nats-account-rule.md new file mode 100644 index 000000000..12f071d7b --- /dev/null +++ b/docs/potctl_md/potctl_delete_nats-account-rule.md @@ -0,0 +1,38 @@ +## potctl delete nats-account-rule + +Delete a NATS account rule + +### Synopsis + +Delete a NATS account rule from the Controller. + +``` +potctl delete nats-account-rule NAME [flags] +``` + +### Examples + +``` +potctl delete nats-account-rule NAME +``` + +### Options + +``` + -h, --help help for nats-account-rule +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_nats-user-rule.md b/docs/potctl_md/potctl_delete_nats-user-rule.md new file mode 100644 index 000000000..a842ca89e --- /dev/null +++ b/docs/potctl_md/potctl_delete_nats-user-rule.md @@ -0,0 +1,38 @@ +## potctl delete nats-user-rule + +Delete a NATS user rule + +### Synopsis + +Delete a NATS user rule from the Controller. + +``` +potctl delete nats-user-rule NAME [flags] +``` + +### Examples + +``` +potctl delete nats-user-rule NAME +``` + +### Options + +``` + -h, --help help for nats-user-rule +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_registry.md b/docs/potctl_md/potctl_delete_registry.md new file mode 100644 index 000000000..4fb6cb085 --- /dev/null +++ b/docs/potctl_md/potctl_delete_registry.md @@ -0,0 +1,38 @@ +## potctl delete registry + +Delete a Registry + +### Synopsis + +Delete a Registry from the Controller. + +``` +potctl delete registry ID [flags] +``` + +### Examples + +``` +potctl delete registry ID +``` + +### Options + +``` + -h, --help help for registry +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_role.md b/docs/potctl_md/potctl_delete_role.md new file mode 100644 index 000000000..2c0aa4c87 --- /dev/null +++ b/docs/potctl_md/potctl_delete_role.md @@ -0,0 +1,38 @@ +## potctl delete role + +Delete a Role + +### Synopsis + +Delete a Role from the Controller. + +``` +potctl delete role NAME [flags] +``` + +### Examples + +``` +potctl delete role NAME +``` + +### Options + +``` + -h, --help help for role +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_rolebinding.md b/docs/potctl_md/potctl_delete_rolebinding.md new file mode 100644 index 000000000..d41d15d0c --- /dev/null +++ b/docs/potctl_md/potctl_delete_rolebinding.md @@ -0,0 +1,38 @@ +## potctl delete rolebinding + +Delete a RoleBinding + +### Synopsis + +Delete a RoleBinding from the Controller. + +``` +potctl delete rolebinding NAME [flags] +``` + +### Examples + +``` +potctl delete rolebinding NAME +``` + +### Options + +``` + -h, --help help for rolebinding +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_secret.md b/docs/potctl_md/potctl_delete_secret.md new file mode 100644 index 000000000..50ba8e26e --- /dev/null +++ b/docs/potctl_md/potctl_delete_secret.md @@ -0,0 +1,38 @@ +## potctl delete secret + +Delete a Secret + +### Synopsis + +Delete a Secret from the Controller. + +``` +potctl delete secret NAME [flags] +``` + +### Examples + +``` +potctl delete secret NAME +``` + +### Options + +``` + -h, --help help for secret +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_service.md b/docs/potctl_md/potctl_delete_service.md new file mode 100644 index 000000000..55082fbbd --- /dev/null +++ b/docs/potctl_md/potctl_delete_service.md @@ -0,0 +1,38 @@ +## potctl delete service + +Delete a Service + +### Synopsis + +Delete a Service from the Controller. + +``` +potctl delete service NAME [flags] +``` + +### Examples + +``` +potctl delete service NAME +``` + +### Options + +``` + -h, --help help for service +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_serviceaccount.md b/docs/potctl_md/potctl_delete_serviceaccount.md new file mode 100644 index 000000000..4251f912b --- /dev/null +++ b/docs/potctl_md/potctl_delete_serviceaccount.md @@ -0,0 +1,38 @@ +## potctl delete serviceaccount + +Delete a ServiceAccount + +### Synopsis + +Delete a ServiceAccount from the Controller. ServiceAccounts are application-scoped; use APPLICATION_NAME/SERVICE_ACCOUNT_NAME (e.g. myapp/my-sa). + +``` +potctl delete serviceaccount APPLICATION_NAME/SERVICE_ACCOUNT_NAME [flags] +``` + +### Examples + +``` +potctl delete serviceaccount myapp/my-sa +``` + +### Options + +``` + -h, --help help for serviceaccount +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_volume-mount.md b/docs/potctl_md/potctl_delete_volume-mount.md new file mode 100644 index 000000000..d4bb8d373 --- /dev/null +++ b/docs/potctl_md/potctl_delete_volume-mount.md @@ -0,0 +1,38 @@ +## potctl delete volume-mount + +Delete a Volume Mount + +### Synopsis + +Delete a Volume Mount from the Controller. + +``` +potctl delete volume-mount NAME [flags] +``` + +### Examples + +``` +potctl delete volume-mount NAME +``` + +### Options + +``` + -h, --help help for volume-mount +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_volume.md b/docs/potctl_md/potctl_delete_volume.md new file mode 100644 index 000000000..f1ac5e406 --- /dev/null +++ b/docs/potctl_md/potctl_delete_volume.md @@ -0,0 +1,40 @@ +## potctl delete volume + +Delete an Volume + +### Synopsis + +Delete an Volume. + +The Volume will be deleted from the Agents that it is stored on. + +``` +potctl delete volume NAME [flags] +``` + +### Examples + +``` +potctl delete volume NAME +``` + +### Options + +``` + -h, --help help for volume +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_deploy.md b/docs/potctl_md/potctl_deploy.md new file mode 100644 index 000000000..5aa945ae7 --- /dev/null +++ b/docs/potctl_md/potctl_deploy.md @@ -0,0 +1,51 @@ +## potctl deploy + +Deploy Edge Compute Network components on existing infrastructure + +### Synopsis + +Deploy Edge Compute Network components on existing infrastructure. +Visit https://docs.datasance.com to view all YAML specifications usable with this command. + +``` +potctl deploy [flags] +``` + +### Examples + +``` +potctl deploy -f ecn.yaml + application-template.yaml + application.yaml + microservice.yaml + catalog.yaml + volume.yaml + route.yaml + secret.yaml + configmap.yaml + service.yaml + volume-mount.yaml +``` + +### Options + +``` + -f, --file string YAML file containing specifications for ioFog resources to deploy + -h, --help help for deploy + --no-cache Disable caching for OfflineImage images after download + --transfer-pool int Maximum number of concurrent OfflineImage transfers (default 2) +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - + + diff --git a/docs/potctl_md/potctl_describe.md b/docs/potctl_md/potctl_describe.md new file mode 100644 index 000000000..b4014a45a --- /dev/null +++ b/docs/potctl_md/potctl_describe.md @@ -0,0 +1,53 @@ +## potctl describe + +Get detailed information of an existing resources + +### Synopsis + +Get detailed information of an existing resources. + +Most resources require a working Controller in the Namespace in order to be described. + +### Options + +``` + -h, --help help for describe + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl describe agent](potctl_describe_agent.md) - Get detailed information about an Agent +* [potctl describe agent-config](potctl_describe_agent-config.md) - Get detailed information about an Agent's configuration +* [potctl describe application](potctl_describe_application.md) - Get detailed information about an Application +* [potctl describe application-template](potctl_describe_application-template.md) - Get detailed information about an Application Template +* [potctl describe certificate](potctl_describe_certificate.md) - Get detailed information about a Certificate +* [potctl describe configmap](potctl_describe_configmap.md) - Get detailed information about a ConfigMap +* [potctl describe controller](potctl_describe_controller.md) - Get detailed information about a Controller +* [potctl describe controlplane](potctl_describe_controlplane.md) - Get detailed information about a Control Plane +* [potctl describe microservice](potctl_describe_microservice.md) - Get detailed information about a Microservice +* [potctl describe namespace](potctl_describe_namespace.md) - Get detailed information about a Namespace +* [potctl describe nats-account](potctl_describe_nats-account.md) - Get detailed information about a NATS account +* [potctl describe nats-account-rule](potctl_describe_nats-account-rule.md) - Get detailed information about a NATS account rule +* [potctl describe nats-user](potctl_describe_nats-user.md) - Get detailed information about a NATS user +* [potctl describe nats-user-rule](potctl_describe_nats-user-rule.md) - Get detailed information about a NATS user rule +* [potctl describe registry](potctl_describe_registry.md) - Get detailed information about a Microservice Registry +* [potctl describe role](potctl_describe_role.md) - Get detailed information about a Role +* [potctl describe rolebinding](potctl_describe_rolebinding.md) - Get detailed information about a RoleBinding +* [potctl describe secret](potctl_describe_secret.md) - Get detailed information about a Secret +* [potctl describe service](potctl_describe_service.md) - Get detailed information about a Service +* [potctl describe serviceaccount](potctl_describe_serviceaccount.md) - Get detailed information about a ServiceAccount +* [potctl describe system-microservice](potctl_describe_system-microservice.md) - Get detailed information about a System Microservice +* [potctl describe volume](potctl_describe_volume.md) - Get detailed information about a Volume +* [potctl describe volume-mount](potctl_describe_volume-mount.md) - Get detailed information about a Volume Mount + + diff --git a/docs/md/iofogctl_describe_edge-resource.md b/docs/potctl_md/potctl_describe_agent-config.md similarity index 52% rename from docs/md/iofogctl_describe_edge-resource.md rename to docs/potctl_md/potctl_describe_agent-config.md index a5c4443c2..2f8c028fc 100644 --- a/docs/md/iofogctl_describe_edge-resource.md +++ b/docs/potctl_md/potctl_describe_agent-config.md @@ -1,25 +1,25 @@ -## iofogctl describe edge-resource +## potctl describe agent-config -Get detailed information about an Edge Resource +Get detailed information about an Agent's configuration ### Synopsis -Get detailed information about an Edge Resource. +Get detailed information about an Agent's configuration. ``` -iofogctl describe edge-resource NAME VERSION [flags] +potctl describe agent-config NAME [flags] ``` ### Examples ``` -iofogctl describe edge-resource NAME VERSION +potctl describe agent-config NAME ``` ### Options ``` - -h, --help help for edge-resource + -h, --help help for agent-config -o, --output-file string YAML output file ``` @@ -28,11 +28,11 @@ iofogctl describe edge-resource NAME VERSION ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl + -v, --verbose Toggle for displaying verbose output of potctl ``` ### SEE ALSO -* [iofogctl describe](iofogctl_describe.md) - Get detailed information of an existing resources +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources diff --git a/docs/potctl_md/potctl_describe_agent.md b/docs/potctl_md/potctl_describe_agent.md new file mode 100644 index 000000000..94db2c6e3 --- /dev/null +++ b/docs/potctl_md/potctl_describe_agent.md @@ -0,0 +1,39 @@ +## potctl describe agent + +Get detailed information about an Agent + +### Synopsis + +Get detailed information about a named Agent. + +``` +potctl describe agent NAME [flags] +``` + +### Examples + +``` +potctl describe agent NAME +``` + +### Options + +``` + --detached Specify command is to run against detached resources + -h, --help help for agent + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_application-template.md b/docs/potctl_md/potctl_describe_application-template.md new file mode 100644 index 000000000..b53aa1890 --- /dev/null +++ b/docs/potctl_md/potctl_describe_application-template.md @@ -0,0 +1,38 @@ +## potctl describe application-template + +Get detailed information about an Application Template + +### Synopsis + +Get detailed information about an Application Template. + +``` +potctl describe application-template NAME [flags] +``` + +### Examples + +``` +potctl describe application-template NAME +``` + +### Options + +``` + -h, --help help for application-template + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_application.md b/docs/potctl_md/potctl_describe_application.md new file mode 100644 index 000000000..3c216c360 --- /dev/null +++ b/docs/potctl_md/potctl_describe_application.md @@ -0,0 +1,38 @@ +## potctl describe application + +Get detailed information about an Application + +### Synopsis + +Get detailed information about an Application. + +``` +potctl describe application NAME [flags] +``` + +### Examples + +``` +potctl describe application NAME +``` + +### Options + +``` + -h, --help help for application + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_certificate.md b/docs/potctl_md/potctl_describe_certificate.md new file mode 100644 index 000000000..ad8a04f7c --- /dev/null +++ b/docs/potctl_md/potctl_describe_certificate.md @@ -0,0 +1,38 @@ +## potctl describe certificate + +Get detailed information about a Certificate + +### Synopsis + +Get detailed information about a Certificate. + +``` +potctl describe certificate NAME [flags] +``` + +### Examples + +``` +potctl describe certificate NAME +``` + +### Options + +``` + -h, --help help for certificate + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_configmap.md b/docs/potctl_md/potctl_describe_configmap.md new file mode 100644 index 000000000..a7fa4c20a --- /dev/null +++ b/docs/potctl_md/potctl_describe_configmap.md @@ -0,0 +1,38 @@ +## potctl describe configmap + +Get detailed information about a ConfigMap + +### Synopsis + +Get detailed information about a ConfigMap. + +``` +potctl describe configmap NAME [flags] +``` + +### Examples + +``` +potctl describe configmap NAME +``` + +### Options + +``` + -h, --help help for configmap + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_controller.md b/docs/potctl_md/potctl_describe_controller.md new file mode 100644 index 000000000..bca539312 --- /dev/null +++ b/docs/potctl_md/potctl_describe_controller.md @@ -0,0 +1,38 @@ +## potctl describe controller + +Get detailed information about a Controller + +### Synopsis + +Get detailed information about a named Controller. + +``` +potctl describe controller NAME [flags] +``` + +### Examples + +``` +potctl describe controller NAME +``` + +### Options + +``` + -h, --help help for controller + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_controlplane.md b/docs/potctl_md/potctl_describe_controlplane.md new file mode 100644 index 000000000..7b0ab8acc --- /dev/null +++ b/docs/potctl_md/potctl_describe_controlplane.md @@ -0,0 +1,38 @@ +## potctl describe controlplane + +Get detailed information about a Control Plane + +### Synopsis + +Get detailed information about the Control Plane in a single Namespace. + +``` +potctl describe controlplane [flags] +``` + +### Examples + +``` +potctl describe controlplane +``` + +### Options + +``` + -h, --help help for controlplane + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_microservice.md b/docs/potctl_md/potctl_describe_microservice.md new file mode 100644 index 000000000..3a73b08f2 --- /dev/null +++ b/docs/potctl_md/potctl_describe_microservice.md @@ -0,0 +1,38 @@ +## potctl describe microservice + +Get detailed information about a Microservice + +### Synopsis + +Get detailed information about a Microservice. + +``` +potctl describe microservice NAME [flags] +``` + +### Examples + +``` +potctl describe microservice NAME +``` + +### Options + +``` + -h, --help help for microservice + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_namespace.md b/docs/potctl_md/potctl_describe_namespace.md new file mode 100644 index 000000000..0f933c8de --- /dev/null +++ b/docs/potctl_md/potctl_describe_namespace.md @@ -0,0 +1,38 @@ +## potctl describe namespace + +Get detailed information about a Namespace + +### Synopsis + +Get detailed information about a Namespace. + +``` +potctl describe namespace NAME [flags] +``` + +### Examples + +``` +potctl describe namespace NAME +``` + +### Options + +``` + -h, --help help for namespace + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_nats-account-rule.md b/docs/potctl_md/potctl_describe_nats-account-rule.md new file mode 100644 index 000000000..de72bf57c --- /dev/null +++ b/docs/potctl_md/potctl_describe_nats-account-rule.md @@ -0,0 +1,32 @@ +## potctl describe nats-account-rule + +Get detailed information about a NATS account rule + +### Synopsis + +Get detailed information about a NATS account rule. + +``` +potctl describe nats-account-rule NAME [flags] +``` + +### Options + +``` + -h, --help help for nats-account-rule + --output string Output format: yaml|json|wide (default "yaml") +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_nats-account.md b/docs/potctl_md/potctl_describe_nats-account.md new file mode 100644 index 000000000..bc17d9f7b --- /dev/null +++ b/docs/potctl_md/potctl_describe_nats-account.md @@ -0,0 +1,33 @@ +## potctl describe nats-account + +Get detailed information about a NATS account + +### Synopsis + +Get detailed information about a NATS account. + +``` +potctl describe nats-account APP_NAME [flags] +``` + +### Options + +``` + -h, --help help for nats-account + --jwt Output decoded JWT payload only + --output string Output format: yaml|json|wide (default "yaml") +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_nats-user-rule.md b/docs/potctl_md/potctl_describe_nats-user-rule.md new file mode 100644 index 000000000..c1c100095 --- /dev/null +++ b/docs/potctl_md/potctl_describe_nats-user-rule.md @@ -0,0 +1,32 @@ +## potctl describe nats-user-rule + +Get detailed information about a NATS user rule + +### Synopsis + +Get detailed information about a NATS user rule. + +``` +potctl describe nats-user-rule NAME [flags] +``` + +### Options + +``` + -h, --help help for nats-user-rule + --output string Output format: yaml|json|wide (default "yaml") +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_nats-user.md b/docs/potctl_md/potctl_describe_nats-user.md new file mode 100644 index 000000000..9111c88f8 --- /dev/null +++ b/docs/potctl_md/potctl_describe_nats-user.md @@ -0,0 +1,33 @@ +## potctl describe nats-user + +Get detailed information about a NATS user + +### Synopsis + +Get detailed information about a NATS user. + +``` +potctl describe nats-user APP_NAME USER_NAME [flags] +``` + +### Options + +``` + -h, --help help for nats-user + --jwt Output decoded JWT payload only + --output string Output format: yaml|json|wide (default "yaml") +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_registry.md b/docs/potctl_md/potctl_describe_registry.md new file mode 100644 index 000000000..645935a52 --- /dev/null +++ b/docs/potctl_md/potctl_describe_registry.md @@ -0,0 +1,38 @@ +## potctl describe registry + +Get detailed information about a Microservice Registry + +### Synopsis + +Get detailed information about a Microservice Registry. + +``` +potctl describe registry NAME [flags] +``` + +### Examples + +``` +potctl describe registry NAME +``` + +### Options + +``` + -h, --help help for registry + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_role.md b/docs/potctl_md/potctl_describe_role.md new file mode 100644 index 000000000..fdee5027c --- /dev/null +++ b/docs/potctl_md/potctl_describe_role.md @@ -0,0 +1,38 @@ +## potctl describe role + +Get detailed information about a Role + +### Synopsis + +Get detailed information about a Role. + +``` +potctl describe role NAME [flags] +``` + +### Examples + +``` +potctl describe role NAME +``` + +### Options + +``` + -h, --help help for role + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_rolebinding.md b/docs/potctl_md/potctl_describe_rolebinding.md new file mode 100644 index 000000000..91acd8f65 --- /dev/null +++ b/docs/potctl_md/potctl_describe_rolebinding.md @@ -0,0 +1,38 @@ +## potctl describe rolebinding + +Get detailed information about a RoleBinding + +### Synopsis + +Get detailed information about a RoleBinding. + +``` +potctl describe rolebinding NAME [flags] +``` + +### Examples + +``` +potctl describe rolebinding NAME +``` + +### Options + +``` + -h, --help help for rolebinding + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_secret.md b/docs/potctl_md/potctl_describe_secret.md new file mode 100644 index 000000000..9ba036b4f --- /dev/null +++ b/docs/potctl_md/potctl_describe_secret.md @@ -0,0 +1,38 @@ +## potctl describe secret + +Get detailed information about a Secret + +### Synopsis + +Get detailed information about a Secret. + +``` +potctl describe secret NAME [flags] +``` + +### Examples + +``` +potctl describe secret NAME +``` + +### Options + +``` + -h, --help help for secret + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_service.md b/docs/potctl_md/potctl_describe_service.md new file mode 100644 index 000000000..9231e8fd7 --- /dev/null +++ b/docs/potctl_md/potctl_describe_service.md @@ -0,0 +1,38 @@ +## potctl describe service + +Get detailed information about a Service + +### Synopsis + +Get detailed information about a Service. + +``` +potctl describe service NAME [flags] +``` + +### Examples + +``` +potctl describe service NAME +``` + +### Options + +``` + -h, --help help for service + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_serviceaccount.md b/docs/potctl_md/potctl_describe_serviceaccount.md new file mode 100644 index 000000000..38d513cdf --- /dev/null +++ b/docs/potctl_md/potctl_describe_serviceaccount.md @@ -0,0 +1,38 @@ +## potctl describe serviceaccount + +Get detailed information about a ServiceAccount + +### Synopsis + +Get detailed information about a ServiceAccount. ServiceAccounts are application-scoped; use APPLICATION_NAME/SERVICE_ACCOUNT_NAME (e.g. myapp/my-sa). + +``` +potctl describe serviceaccount APPLICATION_NAME/SERVICE_ACCOUNT_NAME [flags] +``` + +### Examples + +``` +potctl describe serviceaccount myapp/my-sa +``` + +### Options + +``` + -h, --help help for serviceaccount + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_system-microservice.md b/docs/potctl_md/potctl_describe_system-microservice.md new file mode 100644 index 000000000..3ef1d24ce --- /dev/null +++ b/docs/potctl_md/potctl_describe_system-microservice.md @@ -0,0 +1,38 @@ +## potctl describe system-microservice + +Get detailed information about a System Microservice + +### Synopsis + +Get detailed information about a System Microservice. + +``` +potctl describe system-microservice NAME [flags] +``` + +### Examples + +``` +potctl describe system-microservice NAME +``` + +### Options + +``` + -h, --help help for system-microservice + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_volume-mount.md b/docs/potctl_md/potctl_describe_volume-mount.md new file mode 100644 index 000000000..3650e8215 --- /dev/null +++ b/docs/potctl_md/potctl_describe_volume-mount.md @@ -0,0 +1,38 @@ +## potctl describe volume-mount + +Get detailed information about a Volume Mount + +### Synopsis + +Get detailed information about a Volume Mount. + +``` +potctl describe volume-mount NAME [flags] +``` + +### Examples + +``` +potctl describe volume-mount NAME +``` + +### Options + +``` + -h, --help help for volume-mount + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_volume.md b/docs/potctl_md/potctl_describe_volume.md new file mode 100644 index 000000000..15761b34e --- /dev/null +++ b/docs/potctl_md/potctl_describe_volume.md @@ -0,0 +1,38 @@ +## potctl describe volume + +Get detailed information about a Volume + +### Synopsis + +Get detailed information about a Volume. + +``` +potctl describe volume NAME [flags] +``` + +### Examples + +``` +potctl describe volume NAME +``` + +### Options + +``` + -h, --help help for volume + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_detach.md b/docs/potctl_md/potctl_detach.md new file mode 100644 index 000000000..0fcb2dac6 --- /dev/null +++ b/docs/potctl_md/potctl_detach.md @@ -0,0 +1,36 @@ +## potctl detach + +Detach one ioFog resource from another + +### Synopsis + +Detach one ioFog resource from another. + +### Examples + +``` +potctl detach +``` + +### Options + +``` + -h, --help help for detach +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl detach agent](potctl_detach_agent.md) - Detaches an Agent +* [potctl detach exec](potctl_detach_exec.md) - Remove fog debug exec from an Agent +* [potctl detach volume-mount](potctl_detach_volume-mount.md) - Detach a Volume Mount from existing Agents + + diff --git a/docs/potctl_md/potctl_detach_agent.md b/docs/potctl_md/potctl_detach_agent.md new file mode 100644 index 000000000..9f18d762b --- /dev/null +++ b/docs/potctl_md/potctl_detach_agent.md @@ -0,0 +1,45 @@ +## potctl detach agent + +Detaches an Agent + +### Synopsis + +Detaches an Agent. + +The Agent will be deprovisioned from the Controller within the namespace. +The Agent will be removed from Controller. + +You cannot detach unprovisioned Agents. + +The Agent stack will not be uninstalled from the host. + +``` +potctl detach agent NAME [flags] +``` + +### Examples + +``` +potctl detach agent NAME +``` + +### Options + +``` + --force Detach Agent even if it is running Microservices + -h, --help help for agent +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl detach](potctl_detach.md) - Detach one ioFog resource from another + + diff --git a/docs/potctl_md/potctl_detach_exec.md b/docs/potctl_md/potctl_detach_exec.md new file mode 100644 index 000000000..f11df409a --- /dev/null +++ b/docs/potctl_md/potctl_detach_exec.md @@ -0,0 +1,34 @@ +## potctl detach exec + +Remove fog debug exec from an Agent + +### Synopsis + +Remove fog debug exec resources provisioned with attach exec agent. + +### Examples + +``` +potctl detach exec agent AgentName +``` + +### Options + +``` + -h, --help help for exec +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl detach](potctl_detach.md) - Detach one ioFog resource from another +* [potctl detach exec agent](potctl_detach_exec_agent.md) - Remove fog debug exec from an Agent + + diff --git a/docs/potctl_md/potctl_detach_exec_agent.md b/docs/potctl_md/potctl_detach_exec_agent.md new file mode 100644 index 000000000..d14366fa9 --- /dev/null +++ b/docs/potctl_md/potctl_detach_exec_agent.md @@ -0,0 +1,37 @@ +## potctl detach exec agent + +Remove fog debug exec from an Agent + +### Synopsis + +Remove the debug microservice provisioned for Agent exec via DELETE /iofog/{uuid}/exec. + +``` +potctl detach exec agent NAME [flags] +``` + +### Examples + +``` +potctl detach exec agent AgentName +``` + +### Options + +``` + -h, --help help for agent +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl detach exec](potctl_detach_exec.md) - Remove fog debug exec from an Agent + + diff --git a/docs/md/iofogctl_detach_edge-resource.md b/docs/potctl_md/potctl_detach_volume-mount.md similarity index 51% rename from docs/md/iofogctl_detach_edge-resource.md rename to docs/potctl_md/potctl_detach_volume-mount.md index 97962d4fb..57e745197 100644 --- a/docs/md/iofogctl_detach_edge-resource.md +++ b/docs/potctl_md/potctl_detach_volume-mount.md @@ -1,25 +1,25 @@ -## iofogctl detach edge-resource +## potctl detach volume-mount -Detaches an Edge Resource from an Agent +Detach a Volume Mount from existing Agents ### Synopsis -Detaches an Edge Resource from an Agent. +Detach a Volume Mount from existing Agents. ``` -iofogctl detach edge-resource NAME VERSION AGENT_NAME [flags] +potctl detach volume-mount NAME AGENT_NAME1 AGENT_NAME2 [flags] ``` ### Examples ``` -iofogctl detach edge-resource NAME VERSION AGENT_NAME +potctl detach volume-mount NAME AGENT_NAME1 AGENT_NAME2 ``` ### Options ``` - -h, --help help for edge-resource + -h, --help help for volume-mount ``` ### Options inherited from parent commands @@ -27,11 +27,11 @@ iofogctl detach edge-resource NAME VERSION AGENT_NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl + -v, --verbose Toggle for displaying verbose output of potctl ``` ### SEE ALSO -* [iofogctl detach](iofogctl_detach.md) - Detach one ioFog resource from another +* [potctl detach](potctl_detach.md) - Detach one ioFog resource from another diff --git a/docs/potctl_md/potctl_disconnect.md b/docs/potctl_md/potctl_disconnect.md new file mode 100644 index 000000000..8aaf5992d --- /dev/null +++ b/docs/potctl_md/potctl_disconnect.md @@ -0,0 +1,41 @@ +## potctl disconnect + +Disconnect from an ioFog cluster + +### Synopsis + +Disconnect from an ioFog cluster. + +This will remove all client-side information for this Namespace. The Namespace will itself be deleted. +Use the connect command to reconnect after a disconnect. +If you would like to uninstall the Control Plane and/or Agents, use the delete command instead. + +``` +potctl disconnect [flags] +``` + +### Examples + +``` +potctl disconnect -n NAMESPACE +``` + +### Options + +``` + -h, --help help for disconnect +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - + + diff --git a/docs/potctl_md/potctl_exec.md b/docs/potctl_md/potctl_exec.md new file mode 100644 index 000000000..d27a7313f --- /dev/null +++ b/docs/potctl_md/potctl_exec.md @@ -0,0 +1,29 @@ +## potctl exec + +Connect to an Exec Session of a resource + +### Synopsis + +Connect to an Exec Session of a Microservice or Agent. + +### Options + +``` + -h, --help help for exec +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl exec agent](potctl_exec_agent.md) - Open an interactive exec session on an Agent debug shell +* [potctl exec microservice](potctl_exec_microservice.md) - Open an interactive exec session to a Microservice + + diff --git a/docs/potctl_md/potctl_exec_agent.md b/docs/potctl_md/potctl_exec_agent.md new file mode 100644 index 000000000..f2264fe71 --- /dev/null +++ b/docs/potctl_md/potctl_exec_agent.md @@ -0,0 +1,38 @@ +## potctl exec agent + +Open an interactive exec session on an Agent debug shell + +### Synopsis + +Open a WebSocket exec session to the Agent debug microservice. Provisions fog debug exec automatically when it is not already enabled. + +``` +potctl exec agent AgentName [DEBUG_IMAGE] [flags] +``` + +### Examples + +``` +potctl exec agent AgentName +potctl exec agent AgentName ghcr.io/org/debug:latest +``` + +### Options + +``` + -h, --help help for agent +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl exec](potctl_exec.md) - Connect to an Exec Session of a resource + + diff --git a/docs/potctl_md/potctl_exec_microservice.md b/docs/potctl_md/potctl_exec_microservice.md new file mode 100644 index 000000000..c35665ffd --- /dev/null +++ b/docs/potctl_md/potctl_exec_microservice.md @@ -0,0 +1,37 @@ +## potctl exec microservice + +Open an interactive exec session to a Microservice + +### Synopsis + +Open a WebSocket exec session to a running Microservice. No attach step is required. + +``` +potctl exec microservice AppName/MsvcName [flags] +``` + +### Examples + +``` +potctl exec microservice AppName/MicroserviceName +``` + +### Options + +``` + -h, --help help for microservice +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl exec](potctl_exec.md) - Connect to an Exec Session of a resource + + diff --git a/docs/potctl_md/potctl_get.md b/docs/potctl_md/potctl_get.md new file mode 100644 index 000000000..0482a2b41 --- /dev/null +++ b/docs/potctl_md/potctl_get.md @@ -0,0 +1,63 @@ +## potctl get + +Get information of existing resources + +### Synopsis + +Get information of existing resources. + +Resources like Agents will require a working Controller in the namespace to display all information. + +``` +potctl get RESOURCE [flags] +``` + +### Examples + +``` +potctl get all + namespaces + controllers + agents + application-templates + applications + system-applications + microservices + system-microservices + catalog + registries + volumes + secrets + configmaps + services + volume-mounts + certificates + roles + rolebindings + serviceaccounts + nats-accounts + nats-users + nats-account-rules + nats-user-rules +``` + +### Options + +``` + --detached Specify command is to run against detached resources + -h, --help help for get +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - + + diff --git a/docs/potctl_md/potctl_logs.md b/docs/potctl_md/potctl_logs.md new file mode 100644 index 000000000..f13c62d3d --- /dev/null +++ b/docs/potctl_md/potctl_logs.md @@ -0,0 +1,43 @@ +## potctl logs + +Get log contents of deployed resource + +### Synopsis + +Get log contents of deployed resource + +``` +potctl logs RESOURCE NAME [flags] +``` + +### Examples + +``` +potctl logs controller NAME + agent NAME + microservice AppName/MsvcName +``` + +### Options + +``` + --follow Follow log output (default true) + -h, --help help for logs + --since string Start time in ISO 8601 format (e.g., 2024-01-01T00:00:00Z) + --tail int Number of lines to tail (range: 1-5000) (default 100) + --until string End time in ISO 8601 format (e.g., 2024-01-02T00:00:00Z) +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - + + diff --git a/docs/potctl_md/potctl_move.md b/docs/potctl_md/potctl_move.md new file mode 100644 index 000000000..e6eb796af --- /dev/null +++ b/docs/potctl_md/potctl_move.md @@ -0,0 +1,29 @@ +## potctl move + +Move an existing resources inside the current Namespace + +### Synopsis + +Move an existing resources inside the current Namespace + +### Options + +``` + -h, --help help for move +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl move agent](potctl_move_agent.md) - Move an Agent to another Namespace +* [potctl move microservice](potctl_move_microservice.md) - Move a Microservice to another Agent in the same Namespace + + diff --git a/docs/md/iofogctl_attach_edge-resource.md b/docs/potctl_md/potctl_move_agent.md similarity index 51% rename from docs/md/iofogctl_attach_edge-resource.md rename to docs/potctl_md/potctl_move_agent.md index f5feacc74..1022fbab9 100644 --- a/docs/md/iofogctl_attach_edge-resource.md +++ b/docs/potctl_md/potctl_move_agent.md @@ -1,25 +1,26 @@ -## iofogctl attach edge-resource +## potctl move agent -Attach an Edge Resource to an existing Agent +Move an Agent to another Namespace ### Synopsis -Attach an Edge Resource to an existing Agent. +Move an Agent to another Namespace ``` -iofogctl attach edge-resource NAME VERSION AGENT_NAME [flags] +potctl move agent NAME DEST_NAMESPACE [flags] ``` ### Examples ``` -iofogctl attach edge-resource NAME VERSION AGENT_NAME +potctl move agent NAME DEST_NAMESPACE ``` ### Options ``` - -h, --help help for edge-resource + --force Move Agent even if it is running Microservices + -h, --help help for agent ``` ### Options inherited from parent commands @@ -27,11 +28,11 @@ iofogctl attach edge-resource NAME VERSION AGENT_NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl + -v, --verbose Toggle for displaying verbose output of potctl ``` ### SEE ALSO -* [iofogctl attach](iofogctl_attach.md) - Attach one ioFog resource to another +* [potctl move](potctl_move.md) - Move an existing resources inside the current Namespace diff --git a/docs/md/iofogctl_detach_exec_microservice.md b/docs/potctl_md/potctl_move_microservice.md similarity index 55% rename from docs/md/iofogctl_detach_exec_microservice.md rename to docs/potctl_md/potctl_move_microservice.md index 115f7ce91..969868327 100644 --- a/docs/md/iofogctl_detach_exec_microservice.md +++ b/docs/potctl_md/potctl_move_microservice.md @@ -1,19 +1,19 @@ -## iofogctl detach exec microservice +## potctl move microservice -Detach an Exec Session to a Microservice +Move a Microservice to another Agent in the same Namespace ### Synopsis -Detach an Exec Session to an existing Microservice. +Move a Microservice to another Agent in the same Namespace ``` -iofogctl detach exec microservice NAME [flags] +potctl move microservice NAME AGENT_NAME [flags] ``` ### Examples ``` -iofogctl detach exec microservice AppName/MicroserviceName +potctl move microservice NAME AGENT_NAME ``` ### Options @@ -27,11 +27,11 @@ iofogctl detach exec microservice AppName/MicroserviceName ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl + -v, --verbose Toggle for displaying verbose output of potctl ``` ### SEE ALSO -* [iofogctl detach exec](iofogctl_detach_exec.md) - Detach an Exec Session to a resource +* [potctl move](potctl_move.md) - Move an existing resources inside the current Namespace diff --git a/docs/potctl_md/potctl_nats.md b/docs/potctl_md/potctl_nats.md new file mode 100644 index 000000000..0e9bb4886 --- /dev/null +++ b/docs/potctl_md/potctl_nats.md @@ -0,0 +1,39 @@ +## potctl nats + +Manage NATS resources + +### Synopsis + +Manage NATS-specific operations exposed by Controller APIs. Use get/describe/deploy/delete for CRUD-style NATS resources. + +### Examples + +``` +potctl nats operator describe +potctl nats accounts ensure my-app --nats-rule default-account-rule +potctl nats users create my-app service-user +potctl nats users creds my-app service-user +``` + +### Options + +``` + -h, --help help for nats +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl nats accounts](potctl_nats_accounts.md) - NATS account operations +* [potctl nats operator](potctl_nats_operator.md) - NATS operator operations +* [potctl nats users](potctl_nats_users.md) - NATS user operations + + diff --git a/docs/potctl_md/potctl_nats_accounts.md b/docs/potctl_md/potctl_nats_accounts.md new file mode 100644 index 000000000..222d2d9a0 --- /dev/null +++ b/docs/potctl_md/potctl_nats_accounts.md @@ -0,0 +1,34 @@ +## potctl nats accounts + +NATS account operations + +### Synopsis + +NATS-specific account actions for applications. + +### Examples + +``` +potctl nats accounts ensure my-application --nats-rule default-account-rule +``` + +### Options + +``` + -h, --help help for accounts +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl nats](potctl_nats.md) - Manage NATS resources +* [potctl nats accounts ensure](potctl_nats_accounts_ensure.md) - Ensure NATS account for application + + diff --git a/docs/potctl_md/potctl_nats_accounts_ensure.md b/docs/potctl_md/potctl_nats_accounts_ensure.md new file mode 100644 index 000000000..ce29c7a18 --- /dev/null +++ b/docs/potctl_md/potctl_nats_accounts_ensure.md @@ -0,0 +1,29 @@ +## potctl nats accounts ensure + +Ensure NATS account for application + +``` +potctl nats accounts ensure APP_NAME [flags] +``` + +### Options + +``` + -h, --help help for ensure + --nats-rule string NATS account rule name + --output string Output format: yaml|json|wide (default "yaml") +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl nats accounts](potctl_nats_accounts.md) - NATS account operations + + diff --git a/docs/potctl_md/potctl_nats_operator.md b/docs/potctl_md/potctl_nats_operator.md new file mode 100644 index 000000000..b31cdf957 --- /dev/null +++ b/docs/potctl_md/potctl_nats_operator.md @@ -0,0 +1,28 @@ +## potctl nats operator + +NATS operator operations + +### Synopsis + +Inspect NATS operator metadata and JWT information. + +### Options + +``` + -h, --help help for operator +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl nats](potctl_nats.md) - Manage NATS resources +* [potctl nats operator describe](potctl_nats_operator_describe.md) - Describe NATS operator + + diff --git a/docs/potctl_md/potctl_nats_operator_describe.md b/docs/potctl_md/potctl_nats_operator_describe.md new file mode 100644 index 000000000..e01c0b39d --- /dev/null +++ b/docs/potctl_md/potctl_nats_operator_describe.md @@ -0,0 +1,29 @@ +## potctl nats operator describe + +Describe NATS operator + +``` +potctl nats operator describe [flags] +``` + +### Options + +``` + -h, --help help for describe + --jwt Output decoded JWT payload only + --output string Output format: yaml|json|wide (default "yaml") +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl nats operator](potctl_nats_operator.md) - NATS operator operations + + diff --git a/docs/potctl_md/potctl_nats_users.md b/docs/potctl_md/potctl_nats_users.md new file mode 100644 index 000000000..b8a6fd192 --- /dev/null +++ b/docs/potctl_md/potctl_nats_users.md @@ -0,0 +1,40 @@ +## potctl nats users + +NATS user operations + +### Synopsis + +NATS-specific user actions such as create/delete and creds retrieval. + +### Examples + +``` +potctl nats users create my-application service-user +potctl nats users creds my-application service-user +potctl nats users creds my-application service-user -o ./service-user.creds +``` + +### Options + +``` + -h, --help help for users +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl nats](potctl_nats.md) - Manage NATS resources +* [potctl nats users create](potctl_nats_users_create.md) - Create NATS user under an application account +* [potctl nats users create-mqtt-bearer](potctl_nats_users_create-mqtt-bearer.md) - Create MQTT bearer NATS user +* [potctl nats users creds](potctl_nats_users_creds.md) - Fetch NATS creds +* [potctl nats users delete](potctl_nats_users_delete.md) - Delete NATS user from an application account +* [potctl nats users delete-mqtt-bearer](potctl_nats_users_delete-mqtt-bearer.md) - Delete MQTT bearer NATS user + + diff --git a/docs/potctl_md/potctl_nats_users_create-mqtt-bearer.md b/docs/potctl_md/potctl_nats_users_create-mqtt-bearer.md new file mode 100644 index 000000000..443c8f839 --- /dev/null +++ b/docs/potctl_md/potctl_nats_users_create-mqtt-bearer.md @@ -0,0 +1,30 @@ +## potctl nats users create-mqtt-bearer + +Create MQTT bearer NATS user + +``` +potctl nats users create-mqtt-bearer APP_NAME USER_NAME [flags] +``` + +### Options + +``` + --expires-in int Expiry in seconds + -h, --help help for create-mqtt-bearer + --nats-rule string NATS user rule name + --output string Output format: yaml|json|wide (default "yaml") +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl nats users](potctl_nats_users.md) - NATS user operations + + diff --git a/docs/potctl_md/potctl_nats_users_create.md b/docs/potctl_md/potctl_nats_users_create.md new file mode 100644 index 000000000..89619e13c --- /dev/null +++ b/docs/potctl_md/potctl_nats_users_create.md @@ -0,0 +1,30 @@ +## potctl nats users create + +Create NATS user under an application account + +``` +potctl nats users create APP_NAME USER_NAME [flags] +``` + +### Options + +``` + --expires-in int Expiry in seconds + -h, --help help for create + --nats-rule string NATS user rule name + --output string Output format: yaml|json|wide (default "yaml") +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl nats users](potctl_nats_users.md) - NATS user operations + + diff --git a/docs/potctl_md/potctl_nats_users_creds.md b/docs/potctl_md/potctl_nats_users_creds.md new file mode 100644 index 000000000..7415161da --- /dev/null +++ b/docs/potctl_md/potctl_nats_users_creds.md @@ -0,0 +1,28 @@ +## potctl nats users creds + +Fetch NATS creds + +``` +potctl nats users creds APP_NAME USER_NAME [flags] +``` + +### Options + +``` + -h, --help help for creds + -o, --output-file string Destination creds file path (always overwritten) +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl nats users](potctl_nats_users.md) - NATS user operations + + diff --git a/docs/potctl_md/potctl_nats_users_delete-mqtt-bearer.md b/docs/potctl_md/potctl_nats_users_delete-mqtt-bearer.md new file mode 100644 index 000000000..b73a0f759 --- /dev/null +++ b/docs/potctl_md/potctl_nats_users_delete-mqtt-bearer.md @@ -0,0 +1,27 @@ +## potctl nats users delete-mqtt-bearer + +Delete MQTT bearer NATS user + +``` +potctl nats users delete-mqtt-bearer APP_NAME USER_NAME [flags] +``` + +### Options + +``` + -h, --help help for delete-mqtt-bearer +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl nats users](potctl_nats_users.md) - NATS user operations + + diff --git a/docs/potctl_md/potctl_nats_users_delete.md b/docs/potctl_md/potctl_nats_users_delete.md new file mode 100644 index 000000000..ba4a237ff --- /dev/null +++ b/docs/potctl_md/potctl_nats_users_delete.md @@ -0,0 +1,27 @@ +## potctl nats users delete + +Delete NATS user from an application account + +``` +potctl nats users delete APP_NAME USER_NAME [flags] +``` + +### Options + +``` + -h, --help help for delete +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl nats users](potctl_nats_users.md) - NATS user operations + + diff --git a/docs/potctl_md/potctl_prune.md b/docs/potctl_md/potctl_prune.md new file mode 100644 index 000000000..7f67b5a3a --- /dev/null +++ b/docs/potctl_md/potctl_prune.md @@ -0,0 +1,28 @@ +## potctl prune + +prune ioFog resources + +### Synopsis + +prune ioFog resources + +### Options + +``` + -h, --help help for prune +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl prune agent](potctl_prune_agent.md) - Remove all dangling images from Agent + + diff --git a/docs/md/iofogctl_rename_agent.md b/docs/potctl_md/potctl_prune_agent.md similarity index 64% rename from docs/md/iofogctl_rename_agent.md rename to docs/potctl_md/potctl_prune_agent.md index da4eeba97..da3899972 100644 --- a/docs/md/iofogctl_rename_agent.md +++ b/docs/potctl_md/potctl_prune_agent.md @@ -1,19 +1,19 @@ -## iofogctl rename agent +## potctl prune agent -Rename an Agent +Remove all dangling images from Agent ### Synopsis -Rename an Agent +Remove all the images which are not used by existing containers on the specified Agent ``` -iofogctl rename agent NAME NEW_NAME [flags] +potctl prune agent NAME [flags] ``` ### Examples ``` -iofogctl rename agent NAME NEW_NAME +potctl prune agent NAME ``` ### Options @@ -28,11 +28,11 @@ iofogctl rename agent NAME NEW_NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl + -v, --verbose Toggle for displaying verbose output of potctl ``` ### SEE ALSO -* [iofogctl rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed +* [potctl prune](potctl_prune.md) - prune ioFog resources diff --git a/docs/potctl_md/potctl_rebuild.md b/docs/potctl_md/potctl_rebuild.md new file mode 100644 index 000000000..f7f214989 --- /dev/null +++ b/docs/potctl_md/potctl_rebuild.md @@ -0,0 +1,29 @@ +## potctl rebuild + +Rebuilds a microservice or system-microservice + +### Synopsis + +Rebuilds a microservice or system-microservice + +### Options + +``` + -h, --help help for rebuild +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl rebuild microservice](potctl_rebuild_microservice.md) - Rebuilds a microservice +* [potctl rebuild system-microservice](potctl_rebuild_system-microservice.md) - Rebuilds a system microservice + + diff --git a/docs/md/iofogctl_rename_microservice.md b/docs/potctl_md/potctl_rebuild_microservice.md similarity index 60% rename from docs/md/iofogctl_rename_microservice.md rename to docs/potctl_md/potctl_rebuild_microservice.md index b271fffc6..b98954707 100644 --- a/docs/md/iofogctl_rename_microservice.md +++ b/docs/potctl_md/potctl_rebuild_microservice.md @@ -1,19 +1,19 @@ -## iofogctl rename microservice +## potctl rebuild microservice -Rename a Microservice +Rebuilds a microservice ### Synopsis -Rename a Microservice +Rebuilds a microservice ``` -iofogctl rename microservice NAME NEW_NAME [flags] +potctl rebuild microservice AppNAME/MsvcNAME [flags] ``` ### Examples ``` -iofogctl rename microservice NAME NEW_NAME +potctl rebuild microservice AppNAME/MsvcNAME ``` ### Options @@ -27,11 +27,11 @@ iofogctl rename microservice NAME NEW_NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl + -v, --verbose Toggle for displaying verbose output of potctl ``` ### SEE ALSO -* [iofogctl rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed +* [potctl rebuild](potctl_rebuild.md) - Rebuilds a microservice or system-microservice diff --git a/docs/potctl_md/potctl_rebuild_system-microservice.md b/docs/potctl_md/potctl_rebuild_system-microservice.md new file mode 100644 index 000000000..3c6fc92c6 --- /dev/null +++ b/docs/potctl_md/potctl_rebuild_system-microservice.md @@ -0,0 +1,37 @@ +## potctl rebuild system-microservice + +Rebuilds a system microservice + +### Synopsis + +Rebuilds a system microservice + +``` +potctl rebuild system-microservice AppNAME/MsvcNAME [flags] +``` + +### Examples + +``` +potctl rebuild system-microservice AppNAME/MsvcNAME +``` + +### Options + +``` + -h, --help help for system-microservice +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl rebuild](potctl_rebuild.md) - Rebuilds a microservice or system-microservice + + diff --git a/docs/potctl_md/potctl_reconcile.md b/docs/potctl_md/potctl_reconcile.md new file mode 100644 index 000000000..deec22a33 --- /dev/null +++ b/docs/potctl_md/potctl_reconcile.md @@ -0,0 +1,29 @@ +## potctl reconcile + +Retry async platform provisioning for an agent or service + +### Synopsis + +Enqueue a manual platform reconcile and wait for provisioning to complete. + +### Options + +``` + -h, --help help for reconcile +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl reconcile agent](potctl_reconcile_agent.md) - Reconcile fog router/NATS platform for an agent +* [potctl reconcile service](potctl_reconcile_service.md) - Reconcile service hub provisioning + + diff --git a/docs/potctl_md/potctl_reconcile_agent.md b/docs/potctl_md/potctl_reconcile_agent.md new file mode 100644 index 000000000..808181959 --- /dev/null +++ b/docs/potctl_md/potctl_reconcile_agent.md @@ -0,0 +1,33 @@ +## potctl reconcile agent + +Reconcile fog router/NATS platform for an agent + +``` +potctl reconcile agent NAME [flags] +``` + +### Examples + +``` +potctl reconcile agent my-agent +``` + +### Options + +``` + -h, --help help for agent +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl reconcile](potctl_reconcile.md) - Retry async platform provisioning for an agent or service + + diff --git a/docs/potctl_md/potctl_reconcile_service.md b/docs/potctl_md/potctl_reconcile_service.md new file mode 100644 index 000000000..32c3ef6d8 --- /dev/null +++ b/docs/potctl_md/potctl_reconcile_service.md @@ -0,0 +1,33 @@ +## potctl reconcile service + +Reconcile service hub provisioning + +``` +potctl reconcile service NAME [flags] +``` + +### Examples + +``` +potctl reconcile service my-service +``` + +### Options + +``` + -h, --help help for service +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl reconcile](potctl_reconcile.md) - Retry async platform provisioning for an agent or service + + diff --git a/docs/md/iofogctl_rename_namespace.md b/docs/potctl_md/potctl_rollback.md similarity index 56% rename from docs/md/iofogctl_rename_namespace.md rename to docs/potctl_md/potctl_rollback.md index 97db39fc1..ed6d9fa02 100644 --- a/docs/md/iofogctl_rename_namespace.md +++ b/docs/potctl_md/potctl_rollback.md @@ -1,25 +1,25 @@ -## iofogctl rename namespace +## potctl rollback -Rename a Namespace +Rollback ioFog resources ### Synopsis -Rename a Namespace +Rollback ioFog resources to latest versions available. ``` -iofogctl rename namespace NAME NEW_NAME [flags] +potctl rollback RESOURCE NAME [flags] ``` ### Examples ``` -iofogctl rename namespace NAME NEW_NAME +potctl rollback agent NAME ``` ### Options ``` - -h, --help help for namespace + -h, --help help for rollback ``` ### Options inherited from parent commands @@ -27,11 +27,11 @@ iofogctl rename namespace NAME NEW_NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl + -v, --verbose Toggle for displaying verbose output of potctl ``` ### SEE ALSO -* [iofogctl rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed +* [potctl](potctl.md) - diff --git a/docs/potctl_md/potctl_start.md b/docs/potctl_md/potctl_start.md new file mode 100644 index 000000000..d210c9e0a --- /dev/null +++ b/docs/potctl_md/potctl_start.md @@ -0,0 +1,29 @@ +## potctl start + +Starts a resource + +### Synopsis + +Starts a resource + +### Options + +``` + -h, --help help for start +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl start application](potctl_start_application.md) - Starts an application +* [potctl start microservice](potctl_start_microservice.md) - Starts an microservice + + diff --git a/docs/md/iofogctl_rename_application.md b/docs/potctl_md/potctl_start_application.md similarity index 60% rename from docs/md/iofogctl_rename_application.md rename to docs/potctl_md/potctl_start_application.md index 88f1c40d3..94169194f 100644 --- a/docs/md/iofogctl_rename_application.md +++ b/docs/potctl_md/potctl_start_application.md @@ -1,19 +1,19 @@ -## iofogctl rename application +## potctl start application -Rename an Application +Starts an application ### Synopsis -Rename a Application +Starts an application ``` -iofogctl rename application NAME NEW_NAME [flags] +potctl start application NAME [flags] ``` ### Examples ``` -iofogctl rename application NAME NEW_NAME +potctl start application NAME ``` ### Options @@ -27,11 +27,11 @@ iofogctl rename application NAME NEW_NAME ``` --debug Toggle for displaying verbose output of API clients (HTTP and SSH) -n, --namespace string Namespace to execute respective command within (default "default") - -v, --verbose Toggle for displaying verbose output of iofogctl + -v, --verbose Toggle for displaying verbose output of potctl ``` ### SEE ALSO -* [iofogctl rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed +* [potctl start](potctl_start.md) - Starts a resource diff --git a/docs/potctl_md/potctl_start_microservice.md b/docs/potctl_md/potctl_start_microservice.md new file mode 100644 index 000000000..7e9bec343 --- /dev/null +++ b/docs/potctl_md/potctl_start_microservice.md @@ -0,0 +1,37 @@ +## potctl start microservice + +Starts an microservice + +### Synopsis + +Starts an microservice + +``` +potctl start microservice AppNAME/MsvcNAME [flags] +``` + +### Examples + +``` +potctl start microservice AppNAME/MsvcNAME +``` + +### Options + +``` + -h, --help help for microservice +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl start](potctl_start.md) - Starts a resource + + diff --git a/docs/potctl_md/potctl_stop.md b/docs/potctl_md/potctl_stop.md new file mode 100644 index 000000000..7c857b5bc --- /dev/null +++ b/docs/potctl_md/potctl_stop.md @@ -0,0 +1,29 @@ +## potctl stop + +Stops a resource + +### Synopsis + +Stops a resource + +### Options + +``` + -h, --help help for stop +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - +* [potctl stop application](potctl_stop_application.md) - Stop an application +* [potctl stop microservice](potctl_stop_microservice.md) - Stop an microservice + + diff --git a/docs/potctl_md/potctl_stop_application.md b/docs/potctl_md/potctl_stop_application.md new file mode 100644 index 000000000..8910ac8ba --- /dev/null +++ b/docs/potctl_md/potctl_stop_application.md @@ -0,0 +1,37 @@ +## potctl stop application + +Stop an application + +### Synopsis + +Stop an application + +``` +potctl stop application NAME [flags] +``` + +### Examples + +``` +potctl stop application NAME +``` + +### Options + +``` + -h, --help help for application +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl stop](potctl_stop.md) - Stops a resource + + diff --git a/docs/potctl_md/potctl_stop_microservice.md b/docs/potctl_md/potctl_stop_microservice.md new file mode 100644 index 000000000..b6db85d22 --- /dev/null +++ b/docs/potctl_md/potctl_stop_microservice.md @@ -0,0 +1,37 @@ +## potctl stop microservice + +Stop an microservice + +### Synopsis + +Stop an microservice + +``` +potctl stop microservice AppNAME/MsvcNAME [flags] +``` + +### Examples + +``` +potctl stop microservice AppNAME/MsvcNAME +``` + +### Options + +``` + -h, --help help for microservice +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl stop](potctl_stop.md) - Stops a resource + + diff --git a/docs/potctl_md/potctl_upgrade.md b/docs/potctl_md/potctl_upgrade.md new file mode 100644 index 000000000..033b952f2 --- /dev/null +++ b/docs/potctl_md/potctl_upgrade.md @@ -0,0 +1,37 @@ +## potctl upgrade + +Upgrade ioFog resources + +### Synopsis + +Upgrade ioFog resources to latest versions available. + +``` +potctl upgrade RESOURCE NAME [flags] +``` + +### Examples + +``` +potctl upgrade agent NAME +``` + +### Options + +``` + -h, --help help for upgrade +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - + + diff --git a/docs/potctl_md/potctl_version.md b/docs/potctl_md/potctl_version.md new file mode 100644 index 000000000..64a12a4b0 --- /dev/null +++ b/docs/potctl_md/potctl_version.md @@ -0,0 +1,28 @@ +## potctl version + +Get CLI application version + +``` +potctl version [flags] +``` + +### Options + +``` + --ecn Get default package versions and images of all ECN components + -h, --help help for version +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - + + diff --git a/docs/potctl_md/potctl_view.md b/docs/potctl_md/potctl_view.md new file mode 100644 index 000000000..f99dc0b3a --- /dev/null +++ b/docs/potctl_md/potctl_view.md @@ -0,0 +1,27 @@ +## potctl view + +Open EdgeOps Console + +``` +potctl view [flags] +``` + +### Options + +``` + -h, --help help for view +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl](potctl.md) - + + diff --git a/gitHooks/pre-commit b/gitHooks/pre-commit index fdfd493b2..154ecde40 100644 --- a/gitHooks/pre-commit +++ b/gitHooks/pre-commit @@ -11,11 +11,12 @@ set -e echo "Pre commit:" echo "Building..." make lint -make build +make iofogctl potctl echo "Generating docs..." -./bin/iofogctl documentation md -o ./docs/ +./bin/iofogctl documentation md -o ./docs/iofogctl_md +./bin/potctl documentation md -o ./docs/potctl_md echo "Patching docs..." -find ./docs/md -type f | xargs sed -i '' 's/.*Auto generated.*//g' -find ./docs/md -type f | xargs sed -E -i '' 's/(command within \(default).*/\1 "default")/g' +find ./docs/iofogctl_md ./docs/potctl_md -type f | xargs sed -i '' 's/.*Auto generated.*//g' +find ./docs/iofogctl_md ./docs/potctl_md -type f | xargs sed -E -i '' 's/(command within \(default).*/\1 "default")/g' echo "Adding docs to the commit..." -git add ./docs/ +git add ./docs/iofogctl_md ./docs/potctl_md diff --git a/go.mod b/go.mod index e134bac05..080c0209e 100644 --- a/go.mod +++ b/go.mod @@ -1,155 +1,136 @@ module github.com/eclipse-iofog/iofogctl -go 1.24.0 - -toolchain go1.24.3 +go 1.26.4 require ( - github.com/GeertJohan/go.rice v1.0.2 github.com/briandowns/spinner v1.23.1 - github.com/containers/image/v5 v5.32.1 - github.com/docker/docker v27.4.1+incompatible - github.com/docker/go-connections v0.5.0 - github.com/eclipse-iofog/iofog-go-sdk/v3 v3.7.0-beta.0 - github.com/eclipse-iofog/iofog-operator/v3 v3.7.1-beta.1 - github.com/gorilla/websocket v1.5.3 + github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.8 + github.com/eclipse-iofog/iofog-operator/v3 v3.8.0-rc.1 github.com/mitchellh/go-homedir v1.1.0 + github.com/moby/moby/api v1.55.0 + github.com/moby/moby/client v0.5.0 github.com/opencontainers/go-digest v1.0.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/sftp v1.13.10 - github.com/spf13/cobra v1.8.1 + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 github.com/twmb/algoimpl v0.0.0-20170717182524-076353e90b94 - github.com/vmihailenco/msgpack/v5 v5.4.1 - golang.org/x/crypto v0.46.0 - golang.org/x/oauth2 v0.27.0 - golang.org/x/sys v0.40.0 - golang.org/x/term v0.39.0 + go.podman.io/image/v5 v5.40.0 + golang.org/x/crypto v0.53.0 + golang.org/x/term v0.44.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.32.1 k8s.io/apiextensions-apiserver v0.32.1 k8s.io/apimachinery v0.32.1 k8s.io/client-go v0.32.1 sigs.k8s.io/controller-runtime v0.20.4 + sigs.k8s.io/yaml v1.4.0 ) require ( - dario.cat/mergo v1.0.0 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect + cyphar.com/go-pathrs v0.2.4 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/Microsoft/hcsshim v0.12.5 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/containerd/cgroups/v3 v3.0.3 // indirect - github.com/containerd/errdefs v0.1.0 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect - github.com/containers/ocicrypt v1.2.0 // indirect - github.com/containers/storage v1.55.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect - github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect - github.com/cyphar/filepath-securejoin v0.3.1 // indirect - github.com/daaku/go.zipexe v1.0.1 // indirect + github.com/containers/ocicrypt v1.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.2 // indirect + github.com/docker/docker-credential-helpers v0.9.7 // indirect + github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/fatih/color v1.15.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-jose/go-jose/v4 v4.0.2 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/runtime v0.28.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-openapi/validate v0.24.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/go-containerregistry v0.20.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.21.1 // indirect github.com/google/go-intervals v0.0.2 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/fs v0.1.0 // indirect - github.com/letsencrypt/boulder v0.0.0-20240418210053-89b07f4543e0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mattn/go-sqlite3 v1.14.44 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect - github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mistifyio/go-zfs/v4 v4.0.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/capability v0.4.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect - github.com/moby/sys/user v0.2.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oklog/ulid v1.3.1 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runtime-spec v1.2.0 // indirect - github.com/opencontainers/selinux v1.11.0 // indirect - github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opencontainers/runtime-spec v1.3.0 // indirect + github.com/opencontainers/selinux v1.14.1 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/proglottis/gpgme v0.1.3 // indirect - github.com/rivo/uniseg v0.4.7 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/proglottis/gpgme v0.1.6 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect - github.com/sigstore/fulcio v1.4.5 // indirect - github.com/sigstore/rekor v1.3.6 // indirect - github.com/sigstore/sigstore v1.8.4 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect + github.com/sigstore/fulcio v1.8.5 // indirect + github.com/sigstore/protobuf-specs v0.5.0 // indirect + github.com/sigstore/sigstore v1.10.6 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/smallstep/pkcs7 v0.1.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect - github.com/sylabs/sif/v2 v2.18.0 // indirect - github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect - github.com/tchap/go-patricia/v2 v2.3.1 // indirect - github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect - github.com/ulikunitz/xz v0.5.12 // indirect - github.com/vbatts/tar-split v0.11.5 // indirect - github.com/vbauerster/mpb/v8 v8.7.5 // indirect + github.com/sylabs/sif/v2 v2.24.0 // indirect + github.com/tchap/go-patricia/v2 v2.3.3 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/vbatts/tar-split v0.12.3 // indirect + github.com/vbauerster/mpb/v8 v8.12.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.mongodb.org/mongo-driver v1.14.0 // indirect - go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/text v0.33.0 // indirect - golang.org/x/time v0.7.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/grpc v1.68.1 // indirect - google.golang.org/protobuf v1.35.2 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.podman.io/storage v1.63.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -158,7 +139,6 @@ require ( k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) exclude github.com/Sirupsen/logrus v1.4.2 diff --git a/go.sum b/go.sum index 8801b7c4b..2da3f2927 100644 --- a/go.sum +++ b/go.sum @@ -1,165 +1,97 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774 h1:SCbEWT58NSt7d2mcFdvxC9uyrdcTfvBbPLThhkDmXzg= -github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774/go.mod h1:6/0dYRLLXyJjbkIPeeGyoJ/eKOSI0eU6eTlCBYibgd0= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= -github.com/GeertJohan/go.rice v1.0.2 h1:PtRw+Tg3oa3HYwiDBZyvOJ8LdIyf6lAovJJtr7YOAYk= -github.com/GeertJohan/go.rice v1.0.2/go.mod h1:af5vUNlDNkCjOZeSGFgIJxDje9qdjsO6hshx0gTmZt4= +cyphar.com/go-pathrs v0.2.4 h1:iD/mge36swa1UFKdINkr1Frkpp6wZsy3YYEildj9cLY= +cyphar.com/go-pathrs v0.2.4/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0= -github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= -github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= -github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= -github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= -github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= -github.com/containers/image/v5 v5.32.1 h1:fVa7GxRC4BCPGsfSRs4JY12WyeY26SUYQ0NuANaCFrI= -github.com/containers/image/v5 v5.32.1/go.mod h1:v1l73VeMugfj/QtKI+jhYbwnwFCFnNGckvbST3rQ5Hk= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= -github.com/containers/ocicrypt v1.2.0 h1:X14EgRK3xNFvJEfI5O4Qn4T3E25ANudSOZz/sirVuPM= -github.com/containers/ocicrypt v1.2.0/go.mod h1:ZNviigQajtdlxIZGibvblVuIFBKIuUI2M0QM12SD31U= -github.com/containers/storage v1.55.0 h1:wTWZ3YpcQf1F+dSP4KxG9iqDfpQY1otaUXjPpffuhgg= -github.com/containers/storage v1.55.0/go.mod h1:28cB81IDk+y7ok60Of6u52RbCeBRucbFOeLunhER1RQ= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f h1:eHnXnuK47UlSTOQexbzxAZfekVz6i+LKRdj1CU5DPaM= -github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= -github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE= -github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= -github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= -github.com/daaku/go.zipexe v1.0.1 h1:wV4zMsDOI2SZ2m7Tdz1Ps96Zrx+TzaK15VbUaGozw0M= -github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= +github.com/containers/ocicrypt v1.3.0 h1:ps3St6ZWNWhOQ/Kqld6K2wPHt01Mj3AqRTNCZLIWOfo= +github.com/containers/ocicrypt v1.3.0/go.mod h1:PmfuGFpBwnGLnbqBm+QIy2nc8noDJ1Wt6B19la7VBFo= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= -github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs= +github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= -github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= -github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/docker-credential-helpers v0.9.7 h1:jaPIxEIDz5bQeghNAdzz0ETwMMnM4vzjZlxz3pWP4JA= +github.com/docker/docker-credential-helpers v0.9.7/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/eclipse-iofog/iofog-go-sdk/v3 v3.7.0-beta.0 h1:wjzFAC/XeqCnlJNT6T+5wQRcevUed46eiPChQxsKnHI= -github.com/eclipse-iofog/iofog-go-sdk/v3 v3.7.0-beta.0/go.mod h1:QMKVbhVHxFNCVTDgfg/bMsi8ZFxG1/yZgpn1RymlSQc= -github.com/eclipse-iofog/iofog-operator/v3 v3.7.1-beta.1 h1:h6JuTQY6D9kp1JxamE9I7eCRYkhrzqqwE865yfLhxEU= -github.com/eclipse-iofog/iofog-operator/v3 v3.7.1-beta.1/go.mod h1:r5Jp1ToVbxH1jTz7yHi9yUmdQGSzaUuF3+UR0/x9RmY= +github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.8 h1:Dg9PcSVdNOr5X2KCRCFByXg5jHPs4e+bj87B2AAsknQ= +github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.8/go.mod h1:9ae5lnGma/LDsrW/25QOt4aQ3pC9aM0c3ut3N3BwDWM= +github.com/eclipse-iofog/iofog-operator/v3 v3.8.0-rc.1 h1:y4MCeTVezf154WuInmYcxx44QommoRJj22RswvkdJZY= +github.com/eclipse-iofog/iofog-operator/v3 v3.8.0-rc.1/go.mod h1:gKlAFXIdo5H3aVSCkZAaLxYBrvp7QO+CzxeJgzEyfoc= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= -github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= -github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= -github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -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/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.20.0 h1:wRqHpOeVh3DnenOrPy9xDOLdnLatiGuuNRVelR2gSbg= -github.com/google/go-containerregistry v0.20.0/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.21.1 h1:sOt/o9BS2b87FnR7wxXPvRKU1XVJn2QCwOS5g8zQXlc= +github.com/google/go-containerregistry v0.21.1/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -167,7 +99,6 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -175,27 +106,16 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= -github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -204,61 +124,54 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/letsencrypt/boulder v0.0.0-20240418210053-89b07f4543e0 h1:aiPrFdHDCCvigNBCkOWj2lv9Bx5xDp210OANZEoiP0I= -github.com/letsencrypt/boulder v0.0.0-20240418210053-89b07f4543e0/go.mod h1:srVwm2N3DC/tWqQ+igZXDrmKlNRN8X/dmJ1wEZrv760= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= +github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= -github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= +github.com/mistifyio/go-zfs/v4 v4.0.0 h1:sU0+5dX45tdDK5xNZ3HBi95nxUc48FS92qbIZEvpAg4= +github.com/mistifyio/go-zfs/v4 v4.0.0/go.mod h1:weotFtXTHvBwhr9Mv96KYnDkTPBOHFUbm9cBmQpesL0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= +github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= +github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= +github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= +github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= -github.com/moby/sys/user v0.2.0 h1:OnpapJsRp25vkhw8TFG6OLJODNh/3rEwRWtJ3kakwRM= -github.com/moby/sys/user v0.2.0/go.mod h1:RYstrcWOJpVh+6qzUqp2bU3eaRpdiQeKGlKitaH0PM8= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= -github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= -github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= -github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f h1:/UDgs8FGMqwnHagNDPGOlts35QkhAZ8by3DR7nMih7M= -github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= +github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ= +github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -268,208 +181,194 @@ github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1Hbe github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/proglottis/gpgme v0.1.3 h1:Crxx0oz4LKB3QXc5Ea0J19K/3ICfy3ftr5exgUK1AU0= -github.com/proglottis/gpgme v0.1.3/go.mod h1:fPbW/EZ0LvwQtH8Hy7eixhp1eF3G39dtx7GUN+0Gmy0= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/proglottis/gpgme v0.1.6 h1:8WpQ8VWggLdxkuTnW+sZ1r1t92XBNd8GZNDhQ4Rz+98= +github.com/proglottis/gpgme v0.1.6/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= -github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= -github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= -github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sigstore/fulcio v1.4.5 h1:WWNnrOknD0DbruuZWCbN+86WRROpEl3Xts+WT2Ek1yc= -github.com/sigstore/fulcio v1.4.5/go.mod h1:oz3Qwlma8dWcSS/IENR/6SjbW4ipN0cxpRVfgdsjMU8= -github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= -github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= -github.com/sigstore/sigstore v1.8.4 h1:g4ICNpiENFnWxjmBzBDWUn62rNFeny/P77HUC8da32w= -github.com/sigstore/sigstore v1.8.4/go.mod h1:1jIKtkTFEeISen7en+ZPWdDHazqhxco/+v9CNjc7oNg= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -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/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= +github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs= +github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sigstore/fulcio v1.8.5 h1:HYTD1/L5wlBp8JxsWxUf8hmfaNBBF/x3r3p5l6tZwbA= +github.com/sigstore/fulcio v1.8.5/go.mod h1:tSLYK3JsKvJpDW1BsIsVHZgHj+f8TjXARzqIUWSsSPQ= +github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= +github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/sigstore v1.10.6 h1:YWhMQfTrJSK80QB1pbxjYeAwGKx+5UwWPPAY9hrPPZg= +github.com/sigstore/sigstore v1.10.6/go.mod h1:k/mcVVXw3I87dYG/iCVTSW2xTrW7vPzxxGic4KqsqXs= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= +github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/sylabs/sif/v2 v2.18.0 h1:eXugsS1qx7St2Wu/AJ21KnsQiVCpouPlTigABh+6KYI= -github.com/sylabs/sif/v2 v2.18.0/go.mod h1:GOQj7LIBqp15fjqH5i8ZEbLp8SXJi9S+xbRO+QQAdRo= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= -github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= -github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= -github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/sylabs/sif/v2 v2.24.0 h1:1wB5uMDUQYjk8AckTySaDcP9YnpMb1LyDRr1Jt9A10w= +github.com/sylabs/sif/v2 v2.24.0/go.mod h1:DbXWqWZ1hdLSU+K9ipdds5AmZeHWsyxCOj/oQakBa88= +github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= +github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/twmb/algoimpl v0.0.0-20170717182524-076353e90b94 h1:RVeQNVS7eoXqFemL1LnyzV7yuijHlBtiq6lH5T/mljw= github.com/twmb/algoimpl v0.0.0-20170717182524-076353e90b94/go.mod h1:+E0GZE9c8UBk2GYXo9mPIHAtmmBkJlSWCdzLMcsCWV0= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= -github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= -github.com/vbauerster/mpb/v8 v8.7.5 h1:hUF3zaNsuaBBwzEFoCvfuX3cpesQXZC0Phm/JcHZQ+c= -github.com/vbauerster/mpb/v8 v8.7.5/go.mod h1:bRCnR7K+mj5WXKsy0NWB6Or+wctYGvVwKn6huwvxKa0= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw= +github.com/vbatts/tar-split v0.12.3/go.mod h1:sQOc6OlqGCr7HkGx/IDBeKiTIvqhmj8KffNhEXG4Nq0= +github.com/vbauerster/mpb/v8 v8.12.0 h1:+gneY3ifzc88tKDzOtfG8k8gfngCx615S2ZmFM4liWg= +github.com/vbauerster/mpb/v8 v8.12.0/go.mod h1:V02YIuMVo301Y1VE9VtZlD8s84OMsk+EKN6mwvf/588= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= -go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= -go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= -go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.podman.io/image/v5 v5.40.0 h1:gNQvj343Eb4juCitUBkuDz1T82Zpp6nhgMEXzNfCges= +go.podman.io/image/v5 v5.40.0/go.mod h1:qgXf1abXJ+2l01pL8+CljaMKryeo6ahaHO7H51ooKIc= +go.podman.io/storage v1.63.0 h1:bj/pAWFhChbuBmejzno0iQLhU7FevGVXepRXm5pFGeA= +go.podman.io/storage v1.63.0/go.mod h1:z4Z9K+7GhKjWL/Y1O17+4f8a1KGijVeC9hr3tymhSOs= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -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/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 h1:ImUcDPHjTrAqNhlOkSocDLfG9rrNHH7w7uoKWPaWZ8s= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -479,15 +378,10 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= @@ -502,6 +396,8 @@ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= diff --git a/goreleaser.yml b/goreleaser.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/internal/attach/agent/execute.go b/internal/attach/agent/execute.go index 25bdbddbd..5e88727df 100644 --- a/internal/attach/agent/execute.go +++ b/internal/attach/agent/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package attachagent import ( diff --git a/internal/attach/edgeresource/execute.go b/internal/attach/edgeresource/execute.go deleted file mode 100644 index 81b0eec97..000000000 --- a/internal/attach/edgeresource/execute.go +++ /dev/null @@ -1,70 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package attachedgeresource - -import ( - "fmt" - - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/internal/execute" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -type Options struct { - Name string - Version string - Agent string - Namespace string -} - -type executor struct { - Options -} - -func NewExecutor(opt Options) execute.Executor { - return executor{opt} -} - -func (exe executor) GetName() string { - return fmt.Sprintf("%s/%s", exe.Name, exe.Version) -} - -func (exe executor) Execute() error { - util.SpinStart("Attaching Edge Resource") - - // Init client - clt, err := clientutil.NewControllerClient(exe.Namespace) - if err != nil { - return err - } - - // Get Agent UUID - // agentInfo, err := clt.GetAgentByName(exe.Agent, false) - agentInfo, err := clt.GetAgentByName(exe.Agent) - if err != nil { - return err - } - // Attach to agent - req := client.LinkEdgeResourceRequest{ - AgentUUID: agentInfo.UUID, - EdgeResourceName: exe.Name, - EdgeResourceVersion: exe.Version, - } - if err := clt.LinkEdgeResource(req); err != nil { - return err - } - - return nil -} diff --git a/internal/attach/exec/agent/execute.go b/internal/attach/exec/agent/execute.go index 2d621ca5e..d61cbad07 100644 --- a/internal/attach/exec/agent/execute.go +++ b/internal/attach/exec/agent/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package attachexecagent import ( @@ -47,7 +34,7 @@ func (exe *executor) GetName() string { } func (exe *executor) Execute() error { - util.SpinStart("Attaching Exec Session to Agent") + util.SpinStart("Provisioning debug exec for Agent") // Init client clt, err := clientutil.NewControllerClient(exe.namespace) @@ -57,8 +44,7 @@ func (exe *executor) Execute() error { agent, err := clt.GetAgentByName(exe.name) if err != nil { - msg := "%s\nFailed to get Agent by name: %s" - return fmt.Errorf(msg, err.Error()) + return fmt.Errorf("failed to get Agent by name: %w", err) } // Attach Exec Session to Microservice @@ -68,8 +54,7 @@ func (exe *executor) Execute() error { } err = clt.AttachExecToAgent(&req) if err != nil { - msg := "%s\nFailed to attach Exec Session to Agent: %s" - return fmt.Errorf(msg, err.Error()) + return fmt.Errorf("failed to attach Exec Session to Agent: %w", err) } return nil diff --git a/internal/attach/exec/microservice/execute.go b/internal/attach/exec/microservice/execute.go deleted file mode 100644 index 11a7f506c..000000000 --- a/internal/attach/exec/microservice/execute.go +++ /dev/null @@ -1,95 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package attachexecmicroservice - -import ( - "strings" - - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/internal/execute" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -type Options struct { - Name string - Namespace string - Msvc *client.MicroserviceInfo -} - -type executor struct { - name string - namespace string - msvc *client.MicroserviceInfo -} - -func NewExecutor(opt Options) execute.Executor { - return &executor{ - name: opt.Name, - namespace: opt.Namespace, - msvc: opt.Msvc, - } -} - -func (exe *executor) GetName() string { - return exe.name -} - -func (exe *executor) Execute() error { - util.SpinStart("Attaching Exec Session to Microservice") - - // Init client - clt, err := clientutil.NewControllerClient(exe.namespace) - if err != nil { - return err - } - - appName, msvcName, err := clientutil.ParseFQName(exe.name, "Microservice") - if err != nil { - return err - } - - exe.msvc, err = clt.GetMicroserviceByName(appName, msvcName) - isSystem := false - if err != nil { - // Check if error indicates application not found - if strings.Contains(err.Error(), "Invalid application id") { - // Try system application - exe.msvc, err = clt.GetSystemMicroserviceByName(appName, msvcName) - if err != nil { - return err - } - isSystem = true - } else { - // Return other types of errors - return err - } - } - - // Attach Exec Session to Microservice - req := client.AttachExecMicroserviceRequest{ - UUID: exe.msvc.UUID, - } - if isSystem { - if err := clt.AttachExecSystemMicroservice(&req); err != nil { - return err - } - } else { - if err := clt.AttachExecMicroservice(&req); err != nil { - return err - } - } - - return nil -} diff --git a/internal/attach/volumemount/execute.go b/internal/attach/volumemount/execute.go index f24b586ef..ce86c381b 100644 --- a/internal/attach/volumemount/execute.go +++ b/internal/attach/volumemount/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package attachvolumemount import ( diff --git a/internal/auth/connect_login.go b/internal/auth/connect_login.go new file mode 100644 index 000000000..b0873b39e --- /dev/null +++ b/internal/auth/connect_login.go @@ -0,0 +1,19 @@ +package auth + +import ( + "context" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" +) + +// ConnectLogin performs trust-aware iofogUser login and returns controller agents. +func ConnectLogin(ctx context.Context, namespace, endpoint, caFile, email, password string) ([]client.AgentInfo, error) { + clt, err := newControllerAuthClient(ctx, namespace, endpoint, caFile) + if err != nil { + return nil, err + } + if err := clt.bootstrapLogin(email, password); err != nil { + return nil, err + } + return clt.listAgents() +} diff --git a/internal/auth/controller_client.go b/internal/auth/controller_client.go new file mode 100644 index 000000000..5266fbd28 --- /dev/null +++ b/internal/auth/controller_client.go @@ -0,0 +1,193 @@ +package auth + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "time" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/trust" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const controllerRequestTimeout = 30 * time.Second + +type controllerAuthClient struct { + baseURL *url.URL + httpClient *http.Client + accessToken string +} + +func newControllerAuthClient(ctx context.Context, namespace, endpoint, caFile string) (*controllerAuthClient, error) { + baseURL, err := util.GetBaseURL(endpoint) + if err != nil { + return nil, err + } + cfg, err := trust.ResolveConnectTransport(ctx, namespace, endpoint, caFile) + if err != nil { + return nil, err + } + return newControllerAuthClientWithTransport(baseURL, cfg), nil +} + +func newControllerAuthClientWithTransport(baseURL *url.URL, cfg trust.TransportConfig) *controllerAuthClient { + return &controllerAuthClient{ + baseURL: baseURL, + httpClient: &http.Client{ + Timeout: controllerRequestTimeout, + Transport: &http.Transport{ + TLSClientConfig: cfg.TLSConfig, + }, + }, + } +} + +func (c *controllerAuthClient) bootstrapLogin(email, password string) error { + body, err := c.doJSON(http.MethodPost, "/user/login", map[string]string{ + "Content-Type": "application/json", + }, bootstrapLoginRequest{Email: email, Password: password}) + if err != nil { + return fmt.Errorf("bootstrap login: %w", err) + } + var resp client.LoginResponse + if err := json.Unmarshal(body, &resp); err != nil { + return fmt.Errorf("parse login response: %w", err) + } + c.accessToken = resp.AccessToken + return nil +} + +func (c *controllerAuthClient) listAuthUsers() ([]client.AuthUserResponse, error) { + body, err := c.doJSON(http.MethodGet, "/users", authHeaders(c.accessToken), nil) + if err != nil { + return nil, err + } + var users []client.AuthUserResponse + if err := json.Unmarshal(body, &users); err != nil { + return nil, fmt.Errorf("parse auth users: %w", err) + } + return users, nil +} + +func (c *controllerAuthClient) createAuthUser(req client.AuthUserCreateRequest) (client.AuthUserResponse, error) { + body, err := c.doJSON(http.MethodPost, "/users", authHeaders(c.accessToken), req) + if err != nil { + return client.AuthUserResponse{}, err + } + var user client.AuthUserResponse + if err := json.Unmarshal(body, &user); err != nil { + return client.AuthUserResponse{}, fmt.Errorf("parse auth user response: %w", err) + } + return user, nil +} + +func (c *controllerAuthClient) resetAuthUserToken(userID string) (client.AuthUserResetTokenResponse, error) { + body, err := c.doJSON(http.MethodPost, fmt.Sprintf("/users/%s/reset-token", userID), authHeaders(c.accessToken), nil) + if err != nil { + return client.AuthUserResetTokenResponse{}, err + } + var resp client.AuthUserResetTokenResponse + if err := json.Unmarshal(body, &resp); err != nil { + return client.AuthUserResetTokenResponse{}, fmt.Errorf("parse reset token response: %w", err) + } + return resp, nil +} + +func (c *controllerAuthClient) changePassword(req client.ChangePasswordRequest) error { + headers := map[string]string{"Content-Type": "application/json"} + if req.ResetToken == "" { + headers = authHeaders(c.accessToken) + } + _, err := c.doJSON(http.MethodPost, "/user/change-password", headers, req) + if err != nil { + return fmt.Errorf("change password: %w", err) + } + return nil +} + +func (c *controllerAuthClient) listAgents() ([]client.AgentInfo, error) { + body, err := c.doJSON(http.MethodGet, "/iofog-list", authHeaders(c.accessToken), nil) + if err != nil { + return nil, err + } + var response client.ListAgentsResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("parse agents response: %w", err) + } + return response.Agents, nil +} + +type bootstrapLoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +func authHeaders(token string) map[string]string { + return map[string]string{ + "Content-Type": "application/json", + "Authorization": "Bearer " + token, + } +} + +func (c *controllerAuthClient) doJSON(method, requestPath string, headers map[string]string, body any) ([]byte, error) { + if headers == nil { + headers = map[string]string{"Content-Type": "application/json"} + } + requestURL := *c.baseURL + switch parts := splitPathQuery(requestPath); len(parts) { + case 1: + requestURL.Path = path.Join(requestURL.Path, parts[0]) + case 2: + requestURL.Path = path.Join(requestURL.Path, parts[0]) + requestURL.RawQuery = parts[1] + default: + return nil, fmt.Errorf("invalid request path %q", requestPath) + } + + var reqBody io.Reader + if body != nil { + encoded, err := json.Marshal(body) + if err != nil { + return nil, err + } + reqBody = bytes.NewReader(encoded) + } + + req, err := http.NewRequest(method, requestURL.String(), reqBody) + if err != nil { + return nil, err + } + for key, val := range headers { + req.Header.Set(key, val) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("%s %s returned %d: %s", method, requestPath, resp.StatusCode, string(respBody)) + } + return respBody, nil +} + +func splitPathQuery(requestPath string) []string { + for i := 0; i < len(requestPath); i++ { + if requestPath[i] == '?' { + return []string{requestPath[:i], requestPath[i+1:]} + } + } + return []string{requestPath} +} diff --git a/internal/auth/embedded_user.go b/internal/auth/embedded_user.go new file mode 100644 index 000000000..5e70a5ab5 --- /dev/null +++ b/internal/auth/embedded_user.go @@ -0,0 +1,128 @@ +package auth + +import ( + "context" + "fmt" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" +) + +const ( + AuthModeEmbedded = "embedded" + AuthModeExternal = "external" +) + +// EmbeddedAuthSpec holds inputs for EnsureIofogUserEmbedded. +type EmbeddedAuthSpec struct { + Mode string + Bootstrap *rsc.AuthBootstrap + User *rsc.IofogUser +} + +type embeddedAuthClient interface { + bootstrapLogin(email, password string) error + listAuthUsers() ([]client.AuthUserResponse, error) + createAuthUser(req client.AuthUserCreateRequest) (client.AuthUserResponse, error) + resetAuthUserToken(userID string) (client.AuthUserResetTokenResponse, error) + changePassword(req client.ChangePasswordRequest) error +} + +var newEmbeddedAuthClient = func(ctx context.Context, namespace, endpoint string) (embeddedAuthClient, error) { + return newControllerAuthClient(ctx, namespace, endpoint, "") +} + +// EnsureIofogUserEmbedded creates the iofogUser in embedded auth when missing (RFC §9). +func EnsureIofogUserEmbedded(ctx context.Context, namespace, endpoint string, spec EmbeddedAuthSpec) error { + if spec.Mode != AuthModeEmbedded { + return nil + } + if spec.Bootstrap == nil { + return fmt.Errorf("embedded auth bootstrap credentials are required") + } + if spec.User == nil || spec.User.Email == "" { + return fmt.Errorf("iofogUser email is required") + } + + password := spec.User.GetRawPassword() + if password == "" { + var err error + password, err = PromptIofogUserPassword(spec.User.Email) + if err != nil { + return err + } + spec.User.Password = password + spec.User.EncodePassword() + } + + clt, err := newEmbeddedAuthClient(ctx, namespace, endpoint) + if err != nil { + return err + } + + if err := clt.bootstrapLogin(spec.Bootstrap.Username, spec.Bootstrap.Password); err != nil { + return err + } + + users, err := clt.listAuthUsers() + if err != nil { + return err + } + + existing, found := findAuthUserByEmail(users, spec.User.Email) + if found && !existing.MustChangePassword { + return nil + } + + var userID string + if found { + userID = existing.ID + } else { + tempPassword, err := generateTempPasswordFn() + if err != nil { + return fmt.Errorf("generate temporary password: %w", err) + } + + created, err := clt.createAuthUser(client.AuthUserCreateRequest{ + Email: spec.User.Email, + Password: tempPassword, + Groups: []string{"admin"}, + }) + if err != nil { + return err + } + userID = created.ID + if userID == "" { + return fmt.Errorf("controller did not return auth user id for %q", spec.User.Email) + } + } + + return finalizeEmbeddedUserPassword(clt, userID, password) +} + +func finalizeEmbeddedUserPassword(clt embeddedAuthClient, userID, password string) error { + tokenResp, err := clt.resetAuthUserToken(userID) + if err != nil { + return fmt.Errorf("reset auth user token: %w", err) + } + if tokenResp.ResetToken == "" { + return fmt.Errorf("controller did not return reset token for auth user %q", userID) + } + + if err := clt.changePassword(client.ChangePasswordRequest{ + ResetToken: tokenResp.ResetToken, + NewPassword: password, + }); err != nil { + return fmt.Errorf("set iofog user password: %w", err) + } + return nil +} + +func findAuthUserByEmail(users []client.AuthUserResponse, email string) (*client.AuthUserResponse, bool) { + for i := range users { + if users[i].Email == email { + return &users[i], true + } + } + return nil, false +} diff --git a/internal/auth/embedded_user_test.go b/internal/auth/embedded_user_test.go new file mode 100644 index 000000000..0c88a885c --- /dev/null +++ b/internal/auth/embedded_user_test.go @@ -0,0 +1,224 @@ +package auth + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/stretchr/testify/require" + "golang.org/x/term" +) + +const testTempPassword = "TempPass123!" + +type mockEmbeddedAuthClient struct { + logins [][2]string + loginErr error + users []client.AuthUserResponse + created []client.AuthUserCreateRequest + resetTokenCalls []string + resetTokens []client.AuthUserResetTokenResponse + changedPassword []client.ChangePasswordRequest + listCalls int +} + +func (m *mockEmbeddedAuthClient) bootstrapLogin(email, password string) error { + m.logins = append(m.logins, [2]string{email, password}) + if m.loginErr != nil { + return m.loginErr + } + return nil +} + +func (m *mockEmbeddedAuthClient) listAuthUsers() ([]client.AuthUserResponse, error) { + m.listCalls++ + return m.users, nil +} + +func (m *mockEmbeddedAuthClient) createAuthUser(req client.AuthUserCreateRequest) (client.AuthUserResponse, error) { + m.created = append(m.created, req) + return client.AuthUserResponse{ + ID: "user-1", + Email: req.Email, + Groups: req.Groups, + MustChangePassword: true, + }, nil +} + +func (m *mockEmbeddedAuthClient) resetAuthUserToken(userID string) (client.AuthUserResetTokenResponse, error) { + m.resetTokenCalls = append(m.resetTokenCalls, userID) + if len(m.resetTokens) > 0 { + return m.resetTokens[0], nil + } + return client.AuthUserResetTokenResponse{ResetToken: "reset-token-1", ExpiresIn: 900}, nil +} + +func (m *mockEmbeddedAuthClient) changePassword(req client.ChangePasswordRequest) error { + m.changedPassword = append(m.changedPassword, req) + return nil +} + +func TestEnsureIofogUserEmbeddedSkipsExternal(t *testing.T) { + t.Cleanup(resetEmbeddedAuthDeps) + mock := &mockEmbeddedAuthClient{} + newEmbeddedAuthClient = func(context.Context, string, string) (embeddedAuthClient, error) { + return mock, nil + } + + err := EnsureIofogUserEmbedded(context.Background(), "default", "https://controller.example.com", EmbeddedAuthSpec{ + Mode: AuthModeExternal, + User: &rsc.IofogUser{Email: "user@domain.com"}, + }) + require.NoError(t, err) + require.Empty(t, mock.logins) +} + +func TestEnsureIofogUserEmbeddedCreatesWhenMissing(t *testing.T) { + t.Cleanup(resetEmbeddedAuthDeps) + mock := &mockEmbeddedAuthClient{users: []client.AuthUserResponse{}} + newEmbeddedAuthClient = func(context.Context, string, string) (embeddedAuthClient, error) { + return mock, nil + } + generateTempPasswordFn = func() (string, error) { return testTempPassword, nil } + + user := &rsc.IofogUser{Email: "user@domain.com", Password: "LocalTest12!"} + user.EncodePassword() + + err := EnsureIofogUserEmbedded(context.Background(), "default", "https://controller.example.com", EmbeddedAuthSpec{ + Mode: AuthModeEmbedded, + Bootstrap: &rsc.AuthBootstrap{Username: "admin", Password: "BootstrapTest12!"}, + User: user, + }) + require.NoError(t, err) + require.Len(t, mock.logins, 1) + require.Equal(t, "admin", mock.logins[0][0]) + require.Equal(t, "BootstrapTest12!", mock.logins[0][1]) + require.Len(t, mock.created, 1) + require.Equal(t, "user@domain.com", mock.created[0].Email) + require.Equal(t, testTempPassword, mock.created[0].Password) + require.Equal(t, []string{"admin"}, mock.created[0].Groups) + require.Equal(t, []string{"user-1"}, mock.resetTokenCalls) + require.Len(t, mock.changedPassword, 1) + require.Equal(t, "reset-token-1", mock.changedPassword[0].ResetToken) + require.Equal(t, "LocalTest12!", mock.changedPassword[0].NewPassword) + require.Empty(t, mock.changedPassword[0].CurrentPassword) +} + +func TestEnsureIofogUserEmbeddedSkipsWhenReady(t *testing.T) { + t.Cleanup(resetEmbeddedAuthDeps) + mock := &mockEmbeddedAuthClient{ + users: []client.AuthUserResponse{{ + ID: "user-1", + Email: "user@domain.com", + MustChangePassword: false, + }}, + } + newEmbeddedAuthClient = func(context.Context, string, string) (embeddedAuthClient, error) { + return mock, nil + } + + user := &rsc.IofogUser{Email: "user@domain.com", Password: "LocalTest12!"} + user.EncodePassword() + + err := EnsureIofogUserEmbedded(context.Background(), "default", "https://controller.example.com", EmbeddedAuthSpec{ + Mode: AuthModeEmbedded, + Bootstrap: &rsc.AuthBootstrap{Username: "admin", Password: "BootstrapTest12!"}, + User: user, + }) + require.NoError(t, err) + require.Len(t, mock.logins, 1) + require.Empty(t, mock.created) + require.Empty(t, mock.resetTokenCalls) + require.Empty(t, mock.changedPassword) +} + +func TestEnsureIofogUserEmbeddedFinalizesExistingTempUser(t *testing.T) { + t.Cleanup(resetEmbeddedAuthDeps) + mock := &mockEmbeddedAuthClient{ + users: []client.AuthUserResponse{{ + ID: "existing-user", + Email: "user@domain.com", + MustChangePassword: true, + }}, + } + newEmbeddedAuthClient = func(context.Context, string, string) (embeddedAuthClient, error) { + return mock, nil + } + + user := &rsc.IofogUser{Email: "user@domain.com", Password: "LocalTest12!"} + user.EncodePassword() + + err := EnsureIofogUserEmbedded(context.Background(), "default", "https://controller.example.com", EmbeddedAuthSpec{ + Mode: AuthModeEmbedded, + Bootstrap: &rsc.AuthBootstrap{Username: "admin", Password: "BootstrapTest12!"}, + User: user, + }) + require.NoError(t, err) + require.Empty(t, mock.created) + require.Equal(t, []string{"existing-user"}, mock.resetTokenCalls) + require.Len(t, mock.changedPassword, 1) + require.Equal(t, "LocalTest12!", mock.changedPassword[0].NewPassword) +} + +func TestEnsureIofogUserEmbeddedUsesInteractivePassword(t *testing.T) { + t.Cleanup(resetEmbeddedAuthDeps) + mock := &mockEmbeddedAuthClient{users: []client.AuthUserResponse{}} + newEmbeddedAuthClient = func(context.Context, string, string) (embeddedAuthClient, error) { + return mock, nil + } + generateTempPasswordFn = func() (string, error) { return testTempPassword, nil } + + promptCalls := 0 + readPasswordFn = func(prompt string) (string, error) { + promptCalls++ + if strings.Contains(prompt, "Confirm") { + return "LocalTest12!", nil + } + return "LocalTest12!", nil + } + isTerminalFn = func(int) bool { return true } + + user := &rsc.IofogUser{Email: "user@domain.com"} + err := EnsureIofogUserEmbedded(context.Background(), "default", "https://controller.example.com", EmbeddedAuthSpec{ + Mode: AuthModeEmbedded, + Bootstrap: &rsc.AuthBootstrap{Username: "admin", Password: "BootstrapTest12!"}, + User: user, + }) + require.NoError(t, err) + require.GreaterOrEqual(t, promptCalls, 2) + require.NotEmpty(t, user.GetRawPassword()) + require.Len(t, mock.created, 1) + require.Equal(t, testTempPassword, mock.created[0].Password) + require.Len(t, mock.changedPassword, 1) + require.Equal(t, "LocalTest12!", mock.changedPassword[0].NewPassword) +} + +func TestEnsureIofogUserEmbeddedPropagatesLoginError(t *testing.T) { + t.Cleanup(resetEmbeddedAuthDeps) + newEmbeddedAuthClient = func(context.Context, string, string) (embeddedAuthClient, error) { + return &mockEmbeddedAuthClient{loginErr: errors.New("login failed")}, nil + } + + user := &rsc.IofogUser{Email: "user@domain.com", Password: "LocalTest12!"} + user.EncodePassword() + + err := EnsureIofogUserEmbedded(context.Background(), "default", "https://controller.example.com", EmbeddedAuthSpec{ + Mode: AuthModeEmbedded, + Bootstrap: &rsc.AuthBootstrap{Username: "admin", Password: "BootstrapTest12!"}, + User: user, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "login failed") +} + +func resetEmbeddedAuthDeps() { + newEmbeddedAuthClient = func(ctx context.Context, namespace, endpoint string) (embeddedAuthClient, error) { + return newControllerAuthClient(ctx, namespace, endpoint, "") + } + generateTempPasswordFn = generateTempPassword + readPasswordFn = readPasswordHidden + isTerminalFn = term.IsTerminal +} diff --git a/internal/auth/prompt_password.go b/internal/auth/prompt_password.go new file mode 100644 index 000000000..afb2a0bb3 --- /dev/null +++ b/internal/auth/prompt_password.go @@ -0,0 +1,69 @@ +package auth + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + inputvalidate "github.com/eclipse-iofog/iofogctl/internal/validate" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "golang.org/x/term" +) + +var ( + readPasswordFn = readPasswordHidden + isTerminalFn = term.IsTerminal + stdin = os.Stdin + stdout io.Writer = os.Stdout +) + +// PromptIofogUserPassword interactively collects and confirms an iofogUser password. +func PromptIofogUserPassword(email string) (string, error) { + if !isTerminalFn(int(stdin.Fd())) { + return "", util.NewInputError("iofogUser.password is required in non-interactive mode") + } + + util.SpinHandlePrompt() + defer util.SpinHandlePromptComplete() + + for { + password, err := readPasswordFn(fmt.Sprintf("Enter password for iofog user %s: ", email)) + if err != nil { + return "", err + } + confirm, err := readPasswordFn("Confirm password: ") + if err != nil { + return "", err + } + if password != confirm { + fmt.Fprintln(stdout, "Passwords do not match.") + continue + } + if err := inputvalidate.ValidatePasswordComplexity(password); err != nil { + fmt.Fprintln(stdout, err.Error()) + continue + } + return password, nil + } +} + +func readPasswordHidden(prompt string) (string, error) { + fmt.Fprint(stdout, prompt) + defer fmt.Fprintln(stdout) + + reader := bufio.NewReader(stdin) + if isTerminalFn(int(stdin.Fd())) { + bytes, err := term.ReadPassword(int(stdin.Fd())) + if err != nil { + return "", err + } + return string(bytes), nil + } + line, err := reader.ReadString('\n') + if err != nil && err != io.EOF { + return "", err + } + return strings.TrimSpace(line), nil +} diff --git a/internal/auth/prompt_password_test.go b/internal/auth/prompt_password_test.go new file mode 100644 index 000000000..382451c2c --- /dev/null +++ b/internal/auth/prompt_password_test.go @@ -0,0 +1,84 @@ +package auth + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + + inputvalidate "github.com/eclipse-iofog/iofogctl/internal/validate" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/stretchr/testify/require" + "golang.org/x/term" +) + +func TestPromptIofogUserPasswordNonInteractive(t *testing.T) { + t.Cleanup(resetPromptDeps) + isTerminalFn = func(int) bool { return false } + + _, err := PromptIofogUserPassword("user@domain.com") + require.Error(t, err) + var inputErr *util.InputError + require.ErrorAs(t, err, &inputErr) +} + +func TestPromptIofogUserPasswordMismatchThenSuccess(t *testing.T) { + t.Cleanup(resetPromptDeps) + isTerminalFn = func(int) bool { return true } + stdout = io.Discard + + responses := []string{"LocalTest12!", "WrongConfirm1!", "LocalTest12!", "LocalTest12!"} + readPasswordFn = func(string) (string, error) { + if len(responses) == 0 { + return "", io.EOF + } + next := responses[0] + responses = responses[1:] + return next, nil + } + + password, err := PromptIofogUserPassword("user@domain.com") + require.NoError(t, err) + require.Equal(t, "LocalTest12!", password) +} + +func TestPromptIofogUserPasswordComplexityRetry(t *testing.T) { + t.Cleanup(resetPromptDeps) + isTerminalFn = func(int) bool { return true } + stdout = io.Discard + + responses := []string{"short", "short", "LocalTest12!", "LocalTest12!"} + readPasswordFn = func(string) (string, error) { + next := responses[0] + responses = responses[1:] + return next, nil + } + + password, err := PromptIofogUserPassword("user@domain.com") + require.NoError(t, err) + require.NoError(t, inputvalidate.ValidatePasswordComplexity(password)) +} + +func TestPromptIofogUserPasswordDoesNotPromptBootstrap(t *testing.T) { + t.Cleanup(resetPromptDeps) + var prompts bytes.Buffer + readPasswordFn = func(prompt string) (string, error) { + prompts.WriteString(prompt) + return "LocalTest12!", nil + } + isTerminalFn = func(int) bool { return true } + stdout = io.Discard + + _, err := PromptIofogUserPassword("user@domain.com") + require.NoError(t, err) + combined := prompts.String() + require.True(t, strings.Contains(combined, "iofog user user@domain.com")) + require.False(t, strings.Contains(strings.ToLower(combined), "bootstrap")) +} + +func resetPromptDeps() { + readPasswordFn = readPasswordHidden + isTerminalFn = term.IsTerminal + stdout = os.Stdout +} diff --git a/internal/auth/temp_password.go b/internal/auth/temp_password.go new file mode 100644 index 000000000..579e49719 --- /dev/null +++ b/internal/auth/temp_password.go @@ -0,0 +1,83 @@ +package auth + +import ( + "crypto/rand" + "math/big" + + inputvalidate "github.com/eclipse-iofog/iofogctl/internal/validate" +) + +const tempPasswordLength = 16 + +const ( + tempPasswordUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + tempPasswordLower = "abcdefghijklmnopqrstuvwxyz" + tempPasswordDigits = "0123456789" + tempPasswordSpecial = "!@#$%^&*" +) + +var generateTempPasswordFn = generateTempPassword + +func generateTempPassword() (string, error) { + all := tempPasswordUpper + tempPasswordLower + tempPasswordDigits + tempPasswordSpecial + + required, err := randomChars([]string{ + tempPasswordUpper, + tempPasswordDigits, + tempPasswordSpecial, + }) + if err != nil { + return "", err + } + + password := make([]byte, 0, tempPasswordLength) + password = append(password, required...) + for len(password) < tempPasswordLength { + ch, err := randomCharFrom(all) + if err != nil { + return "", err + } + password = append(password, ch) + } + + if err := shuffleBytes(password); err != nil { + return "", err + } + + result := string(password) + if err := inputvalidate.ValidatePasswordComplexity(result); err != nil { + return "", err + } + return result, nil +} + +func randomChars(charsets []string) ([]byte, error) { + out := make([]byte, 0, len(charsets)) + for _, charset := range charsets { + ch, err := randomCharFrom(charset) + if err != nil { + return nil, err + } + out = append(out, ch) + } + return out, nil +} + +func randomCharFrom(charset string) (byte, error) { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return 0, err + } + return charset[n.Int64()], nil +} + +func shuffleBytes(buf []byte) error { + for i := len(buf) - 1; i > 0; i-- { + j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1))) + if err != nil { + return err + } + buf[i], buf[j.Int64()] = buf[j.Int64()], buf[i] + } + return nil +} diff --git a/internal/auth/temp_password_test.go b/internal/auth/temp_password_test.go new file mode 100644 index 000000000..0961cdd22 --- /dev/null +++ b/internal/auth/temp_password_test.go @@ -0,0 +1,31 @@ +package auth + +import ( + "testing" + "unicode" + + inputvalidate "github.com/eclipse-iofog/iofogctl/internal/validate" + "github.com/stretchr/testify/require" +) + +func TestGenerateTempPasswordMeetsComplexity(t *testing.T) { + for i := 0; i < 20; i++ { + password, err := generateTempPassword() + require.NoError(t, err) + require.GreaterOrEqual(t, len(password), 12) + require.NoError(t, inputvalidate.ValidatePasswordComplexity(password)) + + hasUpper := false + hasDigit := false + for _, r := range password { + if unicode.IsUpper(r) { + hasUpper = true + } + if unicode.IsDigit(r) { + hasDigit = true + } + } + require.True(t, hasUpper) + require.True(t, hasDigit) + } +} diff --git a/internal/cmd/attach.go b/internal/cmd/attach.go index bda74da67..57fcd029d 100644 --- a/internal/cmd/attach.go +++ b/internal/cmd/attach.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -20,7 +7,7 @@ import ( func newAttachCommand() *cobra.Command { cmd := &cobra.Command{ Use: "attach", - Example: `attach`, + Example: ex(`%[1]s attach`), Short: "Attach one ioFog resource to another", Long: `Attach one ioFog resource to another.`, } @@ -28,7 +15,6 @@ func newAttachCommand() *cobra.Command { // Add subcommands cmd.AddCommand( newAttachAgentCommand(), - newAttachEdgeResourceCommand(), newAttachVolumeMountCommand(), newAttachExecCommand(), ) diff --git a/internal/cmd/attach_agent.go b/internal/cmd/attach_agent.go index 57f3a2316..cb5ee7494 100644 --- a/internal/cmd/attach_agent.go +++ b/internal/cmd/attach_agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -27,7 +14,7 @@ func newAttachAgentCommand() *cobra.Command { Long: `Attach a detached Agent to an existing Namespace. The Agent will be provisioned with the Controller within the Namespace.`, - Example: `iofogctl attach agent NAME`, + Example: ex(`%[1]s attach agent NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace of agent diff --git a/internal/cmd/attach_edge_resource.go b/internal/cmd/attach_edge_resource.go deleted file mode 100644 index cdb4c0b03..000000000 --- a/internal/cmd/attach_edge_resource.go +++ /dev/null @@ -1,52 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - "fmt" - - attach "github.com/eclipse-iofog/iofogctl/internal/attach/edgeresource" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" -) - -func newAttachEdgeResourceCommand() *cobra.Command { - opt := attach.Options{} - cmd := &cobra.Command{ - Use: "edge-resource NAME VERSION AGENT_NAME", - Short: "Attach an Edge Resource to an existing Agent", - Long: `Attach an Edge Resource to an existing Agent.`, - Example: `iofogctl attach edge-resource NAME VERSION AGENT_NAME`, - Args: cobra.ExactArgs(3), - Run: func(cmd *cobra.Command, args []string) { - // Get name and namespace of agent - opt.Name = args[0] - opt.Version = args[1] - opt.Agent = args[2] - var err error - opt.Namespace, err = cmd.Flags().GetString("namespace") - util.Check(err) - - // Run the command - exe := attach.NewExecutor(opt) - err = exe.Execute() - util.Check(err) - - msg := fmt.Sprintf("Successfully attached EdgeResource %s/%s to Agent %s", opt.Name, opt.Version, opt.Agent) - util.PrintSuccess(msg) - }, - } - - return cmd -} diff --git a/internal/cmd/attach_exec.go b/internal/cmd/attach_exec.go index ea2f532bb..0280fc3db 100644 --- a/internal/cmd/attach_exec.go +++ b/internal/cmd/attach_exec.go @@ -1,61 +1,20 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( "fmt" attachagent "github.com/eclipse-iofog/iofogctl/internal/attach/exec/agent" - attach "github.com/eclipse-iofog/iofogctl/internal/attach/exec/microservice" "github.com/eclipse-iofog/iofogctl/pkg/util" "github.com/spf13/cobra" ) -func NewAttachExecMicroserviceCommand() *cobra.Command { - opt := attach.Options{} - cmd := &cobra.Command{ - Use: "microservice NAME", - Short: "Attach an Exec Session to a Microservice", - Long: `Attach an Exec Session to an existing Microservice.`, - Example: `iofogctl attach exec microservice AppName/MicroserviceName`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - opt.Name = args[0] - var err error - opt.Namespace, err = cmd.Flags().GetString("namespace") - util.Check(err) - - // Run the command - exe := attach.NewExecutor(opt) - err = exe.Execute() - util.Check(err) - - msg := fmt.Sprintf("Successfully attached Exec Session to Microservice %s", opt.Name) - util.PrintSuccess(msg) - }, - } - - return cmd -} - func newAttachExecAgentCommand() *cobra.Command { opt := attachagent.Options{} cmd := &cobra.Command{ Use: "agent NAME [DEBUG_IMAGE]", - Short: "Attach an Exec Session to an Agent", - Long: `Attach an Exec Session to an existing Agent.`, - Example: `iofogctl attach exec agent AgentName DebugImage`, + Short: "Provision a fog debug exec microservice on an Agent", + Long: `Provision a debug microservice on an Agent for interactive exec via POST /iofog/{uuid}/exec.`, + Example: ex(`%[1]s attach exec agent AgentName DebugImage`), Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { opt.Name = args[0] @@ -66,12 +25,11 @@ func newAttachExecAgentCommand() *cobra.Command { opt.Namespace, err = cmd.Flags().GetString("namespace") util.Check(err) - // Run the command exe := attachagent.NewExecutor(opt) err = exe.Execute() util.Check(err) - msg := fmt.Sprintf("Successfully attached Exec Session to Agent %s", opt.Name) + msg := fmt.Sprintf("Successfully provisioned debug exec for Agent %s", opt.Name) util.PrintSuccess(msg) }, } @@ -82,16 +40,12 @@ func newAttachExecAgentCommand() *cobra.Command { func newAttachExecCommand() *cobra.Command { cmd := &cobra.Command{ Use: "exec", - Short: "Attach an Exec Session to a resource", - Long: `Attach an Exec Session to a Microservice or Agent.`, - Example: `iofogctl attach exec microservice AppName/MicroserviceName`, + Short: "Provision fog debug exec on an Agent", + Long: `Provision fog debug exec resources. Use exec agent to open an interactive shell after provisioning.`, + Example: ex(`%[1]s attach exec agent AgentName`), } - // Add subcommands - cmd.AddCommand( - NewAttachExecMicroserviceCommand(), - newAttachExecAgentCommand(), - ) + cmd.AddCommand(newAttachExecAgentCommand()) return cmd } diff --git a/internal/cmd/attach_volume_moount.go b/internal/cmd/attach_volume_moount.go index 1d5cbd019..ef3dec65a 100644 --- a/internal/cmd/attach_volume_moount.go +++ b/internal/cmd/attach_volume_moount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newAttachVolumeMountCommand() *cobra.Command { Use: "volume-mount NAME AGENT_NAME1 AGENT_NAME2", Short: "Attach a Volume Mount to existing Agents", Long: `Attach a Volume Mount to existing Agents.`, - Example: `iofogctl attach volume-mount NAME AGENT_NAME1 AGENT_NAME2`, + Example: ex(`%[1]s attach volume-mount NAME AGENT_NAME1 AGENT_NAME2`), Args: cobra.MinimumNArgs(2), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace of agent diff --git a/internal/cmd/bash_complete.go b/internal/cmd/bash_complete.go index 5d149bbbb..49c17b0b1 100644 --- a/internal/cmd/bash_complete.go +++ b/internal/cmd/bash_complete.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -29,15 +16,15 @@ func newBashCompleteCommand(rootCmd *cobra.Command) *cobra.Command { home, err := homedir.Dir() util.Check(err) configDir := home + "/.iofog/" - err = os.MkdirAll(configDir, 0755) + err = os.MkdirAll(configDir, util.DirPerm) util.Check(err) cmd := &cobra.Command{ Use: "autocomplete SHELL", Hidden: true, Short: "Generate bash autocomplete file", Long: "Generate bash autocomplete file", - Example: `iofogctl autocomplete bash - zsh`, + Example: ex(`%[1]s autocomplete bash + zsh`), Args: cobra.ExactValidArgs(1), Run: func(cmd *cobra.Command, args []string) { switch t := strings.ToLower(args[0]); t { diff --git a/internal/cmd/configure.go b/internal/cmd/configure.go index 487fb47bd..16e59a30e 100644 --- a/internal/cmd/configure.go +++ b/internal/cmd/configure.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -27,18 +14,19 @@ func newConfigureCommand() *cobra.Command { cmd := &cobra.Command{ Use: "configure RESOURCE NAME", - Short: "Configure iofogctl or ioFog resources", - Long: `Configure iofogctl or ioFog resources + Short: ex("Configure %[1]s or ioFog resources"), + Long: ex(`Configure %[1]s or ioFog resources -If you would like to replace the host value of Remote Controllers or Agents, you should delete and redeploy those resources.`, - Example: `iofogctl configure current-namespace NAME +If you would like to replace the host value of Remote Controllers or Agents, you should delete and redeploy those resources.`), + Example: ex(`%[1]s configure current-namespace NAME -iofogctl configure controller NAME --user USER --key KEYFILE --port PORTNUM +%[1]s configure controller NAME --user USER --key KEYFILE --port PORTNUM controllers agent agents + controlplane -iofogctl configure controlplane --kube FILE`, +%[1]s configure controlplane --kube FILE`), Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { @@ -68,13 +56,15 @@ iofogctl configure controlplane --kube FILE`, err = exe.Execute() util.Check(err) - util.PrintSuccess(fmt.Sprintf("Succesfully configured %s %s", opt.ResourceType, opt.Name)) + util.PrintSuccess(fmt.Sprintf("Successfully configured %s %s", opt.ResourceType, opt.Name)) }, } cmd.Flags().StringVar(&opt.User, "user", "", "Username of remote host") cmd.Flags().StringVar(&opt.KeyFile, "key", "", "Path to private SSH key") cmd.Flags().StringVar(&opt.KubeConfig, "kube", "", "Path to Kubernetes configuration file") - cmd.Flags().IntVar(&opt.Port, "port", 0, "Port number that iofogctl uses to SSH into remote hosts") + cmd.Flags().StringVar(&opt.CAFile, "ca", "", "Path to PEM CA certificate for controller TLS (persisted to namespace config)") + cmd.Flags().StringVar(&opt.CAB64, "ca-b64", "", "Base64-encoded PEM CA certificate for controller TLS (persisted to namespace config)") + cmd.Flags().IntVar(&opt.Port, "port", 0, ex("Port number that %[1]s uses to SSH into remote hosts")) cmd.Flags().Bool("detached", false, pkg.flagDescDetached) return cmd diff --git a/internal/cmd/connect.go b/internal/cmd/connect.go index 9ce7ef837..05b039f7d 100644 --- a/internal/cmd/connect.go +++ b/internal/cmd/connect.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -27,17 +14,17 @@ func newConnectCommand() *cobra.Command { cmd := &cobra.Command{ Use: "connect", Short: "Connect to an existing Control Plane", - Long: `Connect to an existing Control Plane. + Long: ex(`Connect to an existing Control Plane. This command must be executed within an empty or non-existent Namespace. All resources provisioned with the corresponding Control Plane will become visible under the Namespace. -Visit iofog.org to view all YAML specifications usable with this command.`, - Example: `iofogctl connect -f controlplane.yaml +Visit %[2]s to view all YAML specifications usable with this command.`, util.GetCliDocsUrl()), + Example: ex(`%[1]s connect -f controlplane.yaml -iofogctl connect --email EMAIL --pass PASSWORD --kube FILE +%[1]s connect --email EMAIL --pass PASSWORD --kube FILE --email EMAIL --pass PASSWORD --ecn-addr ENDPOINT --name NAME -iofogctl connect --generate`, +%[1]s connect --generate`), Run: func(cmd *cobra.Command, args []string) { var err error opt.Namespace, err = cmd.Flags().GetString("namespace") @@ -61,6 +48,8 @@ iofogctl connect --generate`, cmd.Flags().BoolVar(&opt.OverwriteNamespace, "force", false, "Overwrite existing Namespace") cmd.Flags().BoolVar(&opt.Generate, "generate", false, "Generate a connection string that can be used to connect to this ECN") cmd.Flags().BoolVar(&opt.Base64Encoded, "b64", false, "Indicate whether input password (--pass) is base64 encoded or not") + cmd.Flags().StringVar(&opt.CAFile, "ca", "", "Path to PEM CA certificate for controller TLS (persisted to namespace config)") + cmd.Flags().StringVar(&opt.CAB64, "ca-b64", "", "Base64-encoded PEM CA certificate for controller TLS (persisted to namespace config)") return cmd } diff --git a/internal/cmd/create.go b/internal/cmd/create.go index 1df04dc8e..e72629fd5 100644 --- a/internal/cmd/create.go +++ b/internal/cmd/create.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( diff --git a/internal/cmd/create_namespace.go b/internal/cmd/create_namespace.go index d252e8885..be9e2cded 100644 --- a/internal/cmd/create_namespace.go +++ b/internal/cmd/create_namespace.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -23,12 +10,12 @@ func newCreateNamespaceCommand() *cobra.Command { cmd := &cobra.Command{ Use: "namespace NAME", Short: "Create a Namespace", - Long: `Create a Namespace. + Long: ex(`Create a Namespace. A Namespace contains all components of an Edge Compute Network. -A single instance of iofogctl can be used to manage any number of Edge Compute Networks.`, - Example: `iofogctl create namespace NAME`, +A single instance of %[1]s can be used to manage any number of Edge Compute Networks.`), + Example: ex(`%[1]s create namespace NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace of agent diff --git a/internal/cmd/delete.go b/internal/cmd/delete.go index 7019fd3c6..8117ede67 100644 --- a/internal/cmd/delete.go +++ b/internal/cmd/delete.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -34,6 +21,8 @@ func newDeleteCommand() *cobra.Command { var err error opt.Namespace, err = cmd.Flags().GetString("namespace") util.Check(err) + opt.DeleteNamespace, err = cmd.Flags().GetBool("delete-namespace") + util.Check(err) // Check file if opt.InputFile == "" { @@ -60,7 +49,6 @@ func newDeleteCommand() *cobra.Command { newDeleteRegistryCommand(), newDeleteMicroserviceCommand(), newDeleteVolumeCommand(), - newDeleteEdgeResourceCommand(), newDeleteSecretCommand(), newDeleteConfigMapCommand(), newDeleteRoleCommand(), @@ -75,6 +63,7 @@ func newDeleteCommand() *cobra.Command { // Register flags cmd.Flags().StringVarP(&opt.InputFile, "file", "f", "", pkg.flagDescYaml) + cmd.PersistentFlags().BoolVar(&opt.DeleteNamespace, "delete-namespace", false, `Also delete the Kubernetes namespace (never deletes "default")`) return cmd } diff --git a/internal/cmd/delete_agent.go b/internal/cmd/delete_agent.go index 95cfc2a3f..4fceb913b 100644 --- a/internal/cmd/delete_agent.go +++ b/internal/cmd/delete_agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,14 +11,14 @@ func newDeleteAgentCommand() *cobra.Command { cmd := &cobra.Command{ Use: "agent NAME", Short: "Delete an Agent", - Long: `Delete an Agent. + Long: ex(`Delete an Agent. The Agent will be unprovisioned from the Controller within the namespace. The Agent stack will be uninstalled from the host. -If you wish to not remove the Agent stack from the host, please use iofogctl detach agent`, - Example: `iofogctl delete agent NAME`, +If you wish to not remove the Agent stack from the host, please use %[1]s detach agent`), + Example: ex(`%[1]s delete agent NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace of agent diff --git a/internal/cmd/delete_all.go b/internal/cmd/delete_all.go index 5fa831ab4..aa2776cd1 100644 --- a/internal/cmd/delete_all.go +++ b/internal/cmd/delete_all.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -29,14 +16,16 @@ func newDeleteAllCommand() *cobra.Command { Tears down all components of an Edge Compute Network. If you don't want to tear down the deployments but would like to free up the Namespace, use the disconnect command instead.`, - Example: `iofogctl delete all -n NAMESPACE`, + Example: ex(`%[1]s delete all -n NAMESPACE`), Run: func(cmd *cobra.Command, args []string) { // Execute command namespace, err := cmd.Flags().GetString("namespace") util.Check(err) useDetached, err := cmd.Flags().GetBool("detached") util.Check(err) - err = delete.Execute(namespace, useDetached, force) + deleteNamespace, err := cmd.Flags().GetBool("delete-namespace") + util.Check(err) + err = delete.Execute(namespace, useDetached, force, deleteNamespace) util.Check(err) util.PrintSuccess("Successfully deleted all resources in namespace " + namespace) diff --git a/internal/cmd/delete_application.go b/internal/cmd/delete_application.go index 3d70acacd..2bffa2b71 100644 --- a/internal/cmd/delete_application.go +++ b/internal/cmd/delete_application.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteApplicationCommand() *cobra.Command { Use: "application NAME", Short: "Delete an application", Long: `Delete an application and all its components`, - Example: `iofogctl delete application NAME`, + Example: ex(`%[1]s delete application NAME`), Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get microservice name diff --git a/internal/cmd/delete_catalog_item.go b/internal/cmd/delete_catalog_item.go index 134580c70..8bb54a65d 100644 --- a/internal/cmd/delete_catalog_item.go +++ b/internal/cmd/delete_catalog_item.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteCatalogItemCommand() *cobra.Command { Use: "catalogitem NAME", Short: "Delete a Catalog item", Long: `Delete a Catalog item from the Controller.`, - Example: `iofogctl delete catalogitem NAME`, + Example: ex(`%[1]s delete catalogitem NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace diff --git a/internal/cmd/delete_certificate.go b/internal/cmd/delete_certificate.go index 7ca8b831a..59a2bdf67 100644 --- a/internal/cmd/delete_certificate.go +++ b/internal/cmd/delete_certificate.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteCertificateCommand() *cobra.Command { Use: "certificate NAME", Short: "Delete a Certificate", Long: `Delete a Certificate from the Controller.`, - Example: `iofogctl delete certificate NAME`, + Example: ex(`%[1]s delete certificate NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace diff --git a/internal/cmd/delete_config_map.go b/internal/cmd/delete_config_map.go index 3a041dbb1..1da91fb92 100644 --- a/internal/cmd/delete_config_map.go +++ b/internal/cmd/delete_config_map.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteConfigMapCommand() *cobra.Command { Use: "configmap NAME", Short: "Delete a ConfigMap", Long: `Delete a ConfigMap from the Controller.`, - Example: `iofogctl delete configmap NAME`, + Example: ex(`%[1]s delete configmap NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace diff --git a/internal/cmd/delete_controller.go b/internal/cmd/delete_controller.go index 6a370894d..315d31b47 100644 --- a/internal/cmd/delete_controller.go +++ b/internal/cmd/delete_controller.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteControllerCommand() *cobra.Command { Use: "controller NAME", Short: "Delete a Controller", Long: `Delete a Controller.`, - Example: `iofogctl delete controller NAME`, + Example: ex(`%[1]s delete controller NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace of controller diff --git a/internal/cmd/delete_edge_resource.go b/internal/cmd/delete_edge_resource.go deleted file mode 100644 index 347bf5cb1..000000000 --- a/internal/cmd/delete_edge_resource.go +++ /dev/null @@ -1,52 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - "fmt" - - delete "github.com/eclipse-iofog/iofogctl/internal/delete/edgeresource" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" -) - -func newDeleteEdgeResourceCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "edge-resource NAME VERSION", - Short: "Delete an Edge Resource", - Long: `Delete an Edge Resource. - -Only the specified version will be deleted. -Agents that this Edge Resource are attached to will be notified of the deletion.`, - Example: `iofogctl delete edge-resource NAME VERSION`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - // Get name and namespace of edge resource - name := args[0] - version := args[1] - namespace, err := cmd.Flags().GetString("namespace") - util.Check(err) - - // Run the command - exe := delete.NewExecutor(namespace, name, version) - err = exe.Execute() - util.Check(err) - - msg := fmt.Sprintf("Successfully deleted %s/%s", name, version) - util.PrintSuccess(msg) - }, - } - - return cmd -} diff --git a/internal/cmd/delete_microservice.go b/internal/cmd/delete_microservice.go index 686595309..328571897 100644 --- a/internal/cmd/delete_microservice.go +++ b/internal/cmd/delete_microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteMicroserviceCommand() *cobra.Command { Use: "microservice NAME", Short: "Delete a Microservice", Long: `Delete a Microservice`, - Example: `iofogctl delete microservice NAME`, + Example: ex(`%[1]s delete microservice NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace diff --git a/internal/cmd/delete_namespace.go b/internal/cmd/delete_namespace.go index b8ab8749a..9fc8412d9 100644 --- a/internal/cmd/delete_namespace.go +++ b/internal/cmd/delete_namespace.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -29,7 +16,7 @@ func newDeleteNamespaceCommand() *cobra.Command { The Namespace must be empty. If you would like to delete all resources in the Namespace, use the --force flag.`, - Example: `iofogctl delete namespace NAME`, + Example: ex(`%[1]s delete namespace NAME`), Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get microservice name diff --git a/internal/cmd/delete_nats_account_rule.go b/internal/cmd/delete_nats_account_rule.go index 05b04a710..a7c0374be 100644 --- a/internal/cmd/delete_nats_account_rule.go +++ b/internal/cmd/delete_nats_account_rule.go @@ -11,7 +11,7 @@ func newDeleteNatsAccountRuleCommand() *cobra.Command { Use: "nats-account-rule NAME", Short: "Delete a NATS account rule", Long: `Delete a NATS account rule from the Controller.`, - Example: `iofogctl delete nats-account-rule NAME`, + Example: ex(`%[1]s delete nats-account-rule NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { name := args[0] diff --git a/internal/cmd/delete_nats_user_rule.go b/internal/cmd/delete_nats_user_rule.go index 09d493f83..28d057e6f 100644 --- a/internal/cmd/delete_nats_user_rule.go +++ b/internal/cmd/delete_nats_user_rule.go @@ -11,7 +11,7 @@ func newDeleteNatsUserRuleCommand() *cobra.Command { Use: "nats-user-rule NAME", Short: "Delete a NATS user rule", Long: `Delete a NATS user rule from the Controller.`, - Example: `iofogctl delete nats-user-rule NAME`, + Example: ex(`%[1]s delete nats-user-rule NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { name := args[0] diff --git a/internal/cmd/delete_registry.go b/internal/cmd/delete_registry.go index b36ba90b4..070449d5c 100644 --- a/internal/cmd/delete_registry.go +++ b/internal/cmd/delete_registry.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteRegistryCommand() *cobra.Command { Use: "registry ID", Short: "Delete a Registry", Long: `Delete a Registry from the Controller.`, - Example: `iofogctl delete registry ID`, + Example: ex(`%[1]s delete registry ID`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace diff --git a/internal/cmd/delete_role.go b/internal/cmd/delete_role.go index 2cb7a6699..1f7a491d1 100644 --- a/internal/cmd/delete_role.go +++ b/internal/cmd/delete_role.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteRoleCommand() *cobra.Command { Use: "role NAME", Short: "Delete a Role", Long: `Delete a Role from the Controller.`, - Example: `iofogctl delete role NAME`, + Example: ex(`%[1]s delete role NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { name := args[0] diff --git a/internal/cmd/delete_rolebinding.go b/internal/cmd/delete_rolebinding.go index a14273f2f..55cafef74 100644 --- a/internal/cmd/delete_rolebinding.go +++ b/internal/cmd/delete_rolebinding.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteRoleBindingCommand() *cobra.Command { Use: "rolebinding NAME", Short: "Delete a RoleBinding", Long: `Delete a RoleBinding from the Controller.`, - Example: `iofogctl delete rolebinding NAME`, + Example: ex(`%[1]s delete rolebinding NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { name := args[0] diff --git a/internal/cmd/delete_secret.go b/internal/cmd/delete_secret.go index 00de7e35a..9f13bcf48 100644 --- a/internal/cmd/delete_secret.go +++ b/internal/cmd/delete_secret.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteSecretCommand() *cobra.Command { Use: "secret NAME", Short: "Delete a Secret", Long: `Delete a Secret from the Controller.`, - Example: `iofogctl delete secret NAME`, + Example: ex(`%[1]s delete secret NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace diff --git a/internal/cmd/delete_service.go b/internal/cmd/delete_service.go index cb37f7540..de6baba99 100644 --- a/internal/cmd/delete_service.go +++ b/internal/cmd/delete_service.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteServiceCommand() *cobra.Command { Use: "service NAME", Short: "Delete a Service", Long: `Delete a Service from the Controller.`, - Example: `iofogctl delete service NAME`, + Example: ex(`%[1]s delete service NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace diff --git a/internal/cmd/delete_serviceaccount.go b/internal/cmd/delete_serviceaccount.go index 72b99e142..5b890fab6 100644 --- a/internal/cmd/delete_serviceaccount.go +++ b/internal/cmd/delete_serviceaccount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteServiceAccountCommand() *cobra.Command { Use: "serviceaccount APPLICATION_NAME/SERVICE_ACCOUNT_NAME", Short: "Delete a ServiceAccount", Long: `Delete a ServiceAccount from the Controller. ServiceAccounts are application-scoped; use APPLICATION_NAME/SERVICE_ACCOUNT_NAME (e.g. myapp/my-sa).`, - Example: `iofogctl delete serviceaccount myapp/my-sa`, + Example: ex(`%[1]s delete serviceaccount myapp/my-sa`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { name := args[0] diff --git a/internal/cmd/delete_template.go b/internal/cmd/delete_template.go index e3ac73d8b..6e4a2f996 100644 --- a/internal/cmd/delete_template.go +++ b/internal/cmd/delete_template.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteApplicationTemplateCommand() *cobra.Command { Use: "application-template NAME", Short: "Delete an application-template", Long: `Delete an application-template`, - Example: `iofogctl delete application-template NAME`, + Example: ex(`%[1]s delete application-template NAME`), Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get microservice name diff --git a/internal/cmd/delete_volume.go b/internal/cmd/delete_volume.go index 8be642751..22ee41832 100644 --- a/internal/cmd/delete_volume.go +++ b/internal/cmd/delete_volume.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -26,7 +13,7 @@ func newDeleteVolumeCommand() *cobra.Command { Long: `Delete an Volume. The Volume will be deleted from the Agents that it is stored on.`, - Example: `iofogctl delete volume NAME`, + Example: ex(`%[1]s delete volume NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace of volume diff --git a/internal/cmd/delete_volume_mount.go b/internal/cmd/delete_volume_mount.go index 2586448d2..a339520dc 100644 --- a/internal/cmd/delete_volume_mount.go +++ b/internal/cmd/delete_volume_mount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newDeleteVolumeMountCommand() *cobra.Command { Use: "volume-mount NAME", Short: "Delete a Volume Mount", Long: `Delete a Volume Mount from the Controller.`, - Example: `iofogctl delete volume-mount NAME`, + Example: ex(`%[1]s delete volume-mount NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace diff --git a/internal/cmd/deploy.go b/internal/cmd/deploy.go index 3f729a05f..f684ba4ae 100644 --- a/internal/cmd/deploy.go +++ b/internal/cmd/deploy.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,23 +15,22 @@ func newDeployCommand() *cobra.Command { // Instantiate command cmd := &cobra.Command{ Use: "deploy", - Example: `deploy -f ecn.yaml + Example: ex(`%[1]s deploy -f ecn.yaml application-template.yaml application.yaml microservice.yaml - edge-resource.yaml catalog.yaml volume.yaml route.yaml secret.yaml configmap.yaml service.yaml - volume-mount.yaml`, + volume-mount.yaml`), Args: cobra.ExactArgs(0), Short: "Deploy Edge Compute Network components on existing infrastructure", - Long: `Deploy Edge Compute Network components on existing infrastructure. -Visit iofog.org to view all YAML specifications usable with this command.`, + Long: ex(`Deploy Edge Compute Network components on existing infrastructure. +Visit %[2]s to view all YAML specifications usable with this command.`, util.GetCliDocsUrl()), Run: func(cmd *cobra.Command, args []string) { var err error opt.Namespace, err = cmd.Flags().GetString("namespace") diff --git a/internal/cmd/describe.go b/internal/cmd/describe.go index 78aaf4e0f..78139d44c 100644 --- a/internal/cmd/describe.go +++ b/internal/cmd/describe.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -41,7 +28,6 @@ Most resources require a working Controller in the Namespace in order to be desc newDescribeApplicationCommand(), newDescribeApplicationTemplateCommand(), newDescribeVolumeCommand(), - newDescribeEdgeResourceCommand(), newDescribeSecretCommand(), newDescribeConfigMapCommand(), newDescribeServiceCommand(), diff --git a/internal/cmd/describe_agent.go b/internal/cmd/describe_agent.go index ed3750132..21ab80007 100644 --- a/internal/cmd/describe_agent.go +++ b/internal/cmd/describe_agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeAgentCommand() *cobra.Command { Use: "agent NAME", Short: "Get detailed information about an Agent", Long: `Get detailed information about a named Agent.`, - Example: `iofogctl describe agent NAME`, + Example: ex(`%[1]s describe agent NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_agent_config.go b/internal/cmd/describe_agent_config.go index 536febb84..d46211653 100644 --- a/internal/cmd/describe_agent_config.go +++ b/internal/cmd/describe_agent_config.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeAgentConfigCommand() *cobra.Command { Use: "agent-config NAME", Short: "Get detailed information about an Agent's configuration", Long: `Get detailed information about an Agent's configuration.`, - Example: `iofogctl describe agent-config NAME`, + Example: ex(`%[1]s describe agent-config NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_application.go b/internal/cmd/describe_application.go index cdaf1738d..845ac60f6 100644 --- a/internal/cmd/describe_application.go +++ b/internal/cmd/describe_application.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeApplicationCommand() *cobra.Command { Use: "application NAME", Short: "Get detailed information about an Application", Long: `Get detailed information about an Application.`, - Example: `iofogctl describe application NAME`, + Example: ex(`%[1]s describe application NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_certificate.go b/internal/cmd/describe_certificate.go index ac0b44986..517e812fb 100644 --- a/internal/cmd/describe_certificate.go +++ b/internal/cmd/describe_certificate.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeCertificateCommand() *cobra.Command { Use: "certificate NAME", Short: "Get detailed information about a Certificate", Long: `Get detailed information about a Certificate.`, - Example: `iofogctl describe certificate NAME`, + Example: ex(`%[1]s describe certificate NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_config_map.go b/internal/cmd/describe_config_map.go index f11ab017c..23e919a10 100644 --- a/internal/cmd/describe_config_map.go +++ b/internal/cmd/describe_config_map.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeConfigMapCommand() *cobra.Command { Use: "configmap NAME", Short: "Get detailed information about a ConfigMap", Long: `Get detailed information about a ConfigMap.`, - Example: `iofogctl describe configmap NAME`, + Example: ex(`%[1]s describe configmap NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_controller.go b/internal/cmd/describe_controller.go index 899f70233..2e04677f5 100644 --- a/internal/cmd/describe_controller.go +++ b/internal/cmd/describe_controller.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeControllerCommand() *cobra.Command { Use: "controller NAME", Short: "Get detailed information about a Controller", Long: `Get detailed information about a named Controller.`, - Example: `iofogctl describe controller NAME`, + Example: ex(`%[1]s describe controller NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_controlplane.go b/internal/cmd/describe_controlplane.go index 73c5b8b6a..854825a50 100644 --- a/internal/cmd/describe_controlplane.go +++ b/internal/cmd/describe_controlplane.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeControlPlaneCommand() *cobra.Command { Use: "controlplane", Short: "Get detailed information about a Control Plane", Long: `Get detailed information about the Control Plane in a single Namespace.`, - Example: `iofogctl describe controlplane`, + Example: ex(`%[1]s describe controlplane`), Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_edge_resource.go b/internal/cmd/describe_edge_resource.go deleted file mode 100644 index 634463f89..000000000 --- a/internal/cmd/describe_edge_resource.go +++ /dev/null @@ -1,53 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - "github.com/eclipse-iofog/iofogctl/internal/describe" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" -) - -func newDescribeEdgeResourceCommand() *cobra.Command { - opt := describe.Options{ - Resource: "edge-resource", - } - - cmd := &cobra.Command{ - Use: "edge-resource NAME VERSION", - Short: "Get detailed information about an Edge Resource", - Long: `Get detailed information about an Edge Resource.`, - Example: `iofogctl describe edge-resource NAME VERSION`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - // Get resource type and name - var err error - opt.Name = args[0] - opt.Version = args[1] - opt.Namespace, err = cmd.Flags().GetString("namespace") - util.Check(err) - - // Get executor for describe command - exe, err := describe.NewExecutor(&opt) - util.Check(err) - - // Execute the command - err = exe.Execute() - util.Check(err) - }, - } - cmd.Flags().StringVarP(&opt.Filename, "output-file", "o", "", "YAML output file") - - return cmd -} diff --git a/internal/cmd/describe_microservice.go b/internal/cmd/describe_microservice.go index 535383c72..cc00517c0 100644 --- a/internal/cmd/describe_microservice.go +++ b/internal/cmd/describe_microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeMicroserviceCommand() *cobra.Command { Use: "microservice NAME", Short: "Get detailed information about a Microservice", Long: `Get detailed information about a Microservice.`, - Example: `iofogctl describe microservice NAME`, + Example: ex(`%[1]s describe microservice NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_namespace.go b/internal/cmd/describe_namespace.go index 7387be6b8..a3c44cd5d 100644 --- a/internal/cmd/describe_namespace.go +++ b/internal/cmd/describe_namespace.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeNamespaceCommand() *cobra.Command { Use: "namespace NAME", Short: "Get detailed information about a Namespace", Long: `Get detailed information about a Namespace.`, - Example: `iofogctl describe namespace NAME`, + Example: ex(`%[1]s describe namespace NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_registry.go b/internal/cmd/describe_registry.go index a3f655308..a2f14951a 100644 --- a/internal/cmd/describe_registry.go +++ b/internal/cmd/describe_registry.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeRegistryCommand() *cobra.Command { Use: "registry NAME", Short: "Get detailed information about a Microservice Registry", Long: `Get detailed information about a Microservice Registry.`, - Example: `iofogctl describe registry NAME`, + Example: ex(`%[1]s describe registry NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_role.go b/internal/cmd/describe_role.go index a8767df94..733627dd8 100644 --- a/internal/cmd/describe_role.go +++ b/internal/cmd/describe_role.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeRoleCommand() *cobra.Command { Use: "role NAME", Short: "Get detailed information about a Role", Long: `Get detailed information about a Role.`, - Example: `iofogctl describe role NAME`, + Example: ex(`%[1]s describe role NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error diff --git a/internal/cmd/describe_rolebinding.go b/internal/cmd/describe_rolebinding.go index 8775cabf2..fe67549eb 100644 --- a/internal/cmd/describe_rolebinding.go +++ b/internal/cmd/describe_rolebinding.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeRoleBindingCommand() *cobra.Command { Use: "rolebinding NAME", Short: "Get detailed information about a RoleBinding", Long: `Get detailed information about a RoleBinding.`, - Example: `iofogctl describe rolebinding NAME`, + Example: ex(`%[1]s describe rolebinding NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error diff --git a/internal/cmd/describe_secret.go b/internal/cmd/describe_secret.go index 3d1ed99e6..3be8fc5ff 100644 --- a/internal/cmd/describe_secret.go +++ b/internal/cmd/describe_secret.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeSecretCommand() *cobra.Command { Use: "secret NAME", Short: "Get detailed information about a Secret", Long: `Get detailed information about a Secret.`, - Example: `iofogctl describe secret NAME`, + Example: ex(`%[1]s describe secret NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_service.go b/internal/cmd/describe_service.go index 042187c09..a3fb06ccd 100644 --- a/internal/cmd/describe_service.go +++ b/internal/cmd/describe_service.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeServiceCommand() *cobra.Command { Use: "service NAME", Short: "Get detailed information about a Service", Long: `Get detailed information about a Service.`, - Example: `iofogctl describe service NAME`, + Example: ex(`%[1]s describe service NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_serviceaccount.go b/internal/cmd/describe_serviceaccount.go index 2a4011524..32679eaea 100644 --- a/internal/cmd/describe_serviceaccount.go +++ b/internal/cmd/describe_serviceaccount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeServiceAccountCommand() *cobra.Command { Use: "serviceaccount APPLICATION_NAME/SERVICE_ACCOUNT_NAME", Short: "Get detailed information about a ServiceAccount", Long: `Get detailed information about a ServiceAccount. ServiceAccounts are application-scoped; use APPLICATION_NAME/SERVICE_ACCOUNT_NAME (e.g. myapp/my-sa).`, - Example: `iofogctl describe serviceaccount myapp/my-sa`, + Example: ex(`%[1]s describe serviceaccount myapp/my-sa`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error diff --git a/internal/cmd/describe_system_microservice.go b/internal/cmd/describe_system_microservice.go index ecc6be6fa..c671a2f83 100644 --- a/internal/cmd/describe_system_microservice.go +++ b/internal/cmd/describe_system_microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeSystemMicroserviceCommand() *cobra.Command { Use: "system-microservice NAME", Short: "Get detailed information about a System Microservice", Long: `Get detailed information about a System Microservice.`, - Example: `iofogctl describe system-microservice NAME`, + Example: ex(`%[1]s describe system-microservice NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_template.go b/internal/cmd/describe_template.go index f613f4207..1bf6aedd0 100644 --- a/internal/cmd/describe_template.go +++ b/internal/cmd/describe_template.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeApplicationTemplateCommand() *cobra.Command { Use: "application-template NAME", Short: "Get detailed information about an Application Template", Long: `Get detailed information about an Application Template.`, - Example: `iofogctl describe application-template NAME`, + Example: ex(`%[1]s describe application-template NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_volume.go b/internal/cmd/describe_volume.go index a74f44adc..3979440b3 100644 --- a/internal/cmd/describe_volume.go +++ b/internal/cmd/describe_volume.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeVolumeCommand() *cobra.Command { Use: "volume NAME", Short: "Get detailed information about a Volume", Long: `Get detailed information about a Volume.`, - Example: `iofogctl describe volume NAME`, + Example: ex(`%[1]s describe volume NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/describe_volume_mount.go b/internal/cmd/describe_volume_mount.go index 78f68ac66..98ab735b8 100644 --- a/internal/cmd/describe_volume_mount.go +++ b/internal/cmd/describe_volume_mount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDescribeVolumeMountCommand() *cobra.Command { Use: "volume-mount NAME", Short: "Get detailed information about a Volume Mount", Long: `Get detailed information about a Volume Mount.`, - Example: `iofogctl describe volume-mount NAME`, + Example: ex(`%[1]s describe volume-mount NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/detach.go b/internal/cmd/detach.go index 1069a6b43..1a74f3ecb 100644 --- a/internal/cmd/detach.go +++ b/internal/cmd/detach.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -20,7 +7,7 @@ import ( func newDetachCommand() *cobra.Command { cmd := &cobra.Command{ Use: "detach", - Example: `detach`, + Example: ex(`%[1]s detach`), Short: "Detach one ioFog resource from another", Long: `Detach one ioFog resource from another.`, } @@ -28,7 +15,6 @@ func newDetachCommand() *cobra.Command { // Add subcommands cmd.AddCommand( newDetachAgentCommand(), - newDetachEdgeResourceCommand(), newDetachVolumeMountCommand(), newDetachExecCommand(), ) diff --git a/internal/cmd/detach_agent.go b/internal/cmd/detach_agent.go index 5a72ccfce..8042c9a5a 100644 --- a/internal/cmd/detach_agent.go +++ b/internal/cmd/detach_agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -32,7 +19,7 @@ The Agent will be removed from Controller. You cannot detach unprovisioned Agents. The Agent stack will not be uninstalled from the host.`, - Example: `iofogctl detach agent NAME`, + Example: ex(`%[1]s detach agent NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace of agent diff --git a/internal/cmd/detach_edge_resource.go b/internal/cmd/detach_edge_resource.go deleted file mode 100644 index 16a74a7e3..000000000 --- a/internal/cmd/detach_edge_resource.go +++ /dev/null @@ -1,50 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - "fmt" - - detach "github.com/eclipse-iofog/iofogctl/internal/detach/edgeresource" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" -) - -func newDetachEdgeResourceCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "edge-resource NAME VERSION AGENT_NAME", - Short: "Detaches an Edge Resource from an Agent", - Long: `Detaches an Edge Resource from an Agent.`, - Example: `iofogctl detach edge-resource NAME VERSION AGENT_NAME`, - Args: cobra.ExactArgs(3), - Run: func(cmd *cobra.Command, args []string) { - // Get name and namespace of edge resource - name := args[0] - version := args[1] - agent := args[2] - namespace, err := cmd.Flags().GetString("namespace") - util.Check(err) - - // Run the command - exe := detach.NewExecutor(namespace, name, version, agent) - err = exe.Execute() - util.Check(err) - - msg := fmt.Sprintf("Successfully detached %s/%s", name, version) - util.PrintSuccess(msg) - }, - } - - return cmd -} diff --git a/internal/cmd/detach_exec.go b/internal/cmd/detach_exec.go index 33a44332d..7f4fa9c25 100644 --- a/internal/cmd/detach_exec.go +++ b/internal/cmd/detach_exec.go @@ -1,61 +1,20 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( "fmt" detachagent "github.com/eclipse-iofog/iofogctl/internal/detach/exec/agent" - detach "github.com/eclipse-iofog/iofogctl/internal/detach/exec/microservice" "github.com/eclipse-iofog/iofogctl/pkg/util" "github.com/spf13/cobra" ) -func NewDetachExecMicroserviceCommand() *cobra.Command { - opt := detach.Options{} - cmd := &cobra.Command{ - Use: "microservice NAME", - Short: "Detach an Exec Session to a Microservice", - Long: `Detach an Exec Session to an existing Microservice.`, - Example: `iofogctl detach exec microservice AppName/MicroserviceName`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - opt.Name = args[0] - var err error - opt.Namespace, err = cmd.Flags().GetString("namespace") - util.Check(err) - - // Run the command - exe := detach.NewExecutor(opt) - err = exe.Execute() - util.Check(err) - - msg := fmt.Sprintf("Successfully detached Exec Session from Microservice %s", opt.Name) - util.PrintSuccess(msg) - }, - } - - return cmd -} - func newDetachExecAgentCommand() *cobra.Command { opt := detachagent.Options{} cmd := &cobra.Command{ Use: "agent NAME", - Short: "Detach an Exec Session from an Agent", - Long: `Detach an Exec Session from an existing Agent.`, - Example: `iofogctl detach exec agent AgentName`, + Short: "Remove fog debug exec from an Agent", + Long: `Remove the debug microservice provisioned for Agent exec via DELETE /iofog/{uuid}/exec.`, + Example: ex(`%[1]s detach exec agent AgentName`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { opt.Name = args[0] @@ -63,12 +22,11 @@ func newDetachExecAgentCommand() *cobra.Command { opt.Namespace, err = cmd.Flags().GetString("namespace") util.Check(err) - // Run the command exe := detachagent.NewExecutor(opt) err = exe.Execute() util.Check(err) - msg := fmt.Sprintf("Successfully detached Exec Session from Agent %s", opt.Name) + msg := fmt.Sprintf("Successfully removed debug exec from Agent %s", opt.Name) util.PrintSuccess(msg) }, } @@ -79,16 +37,12 @@ func newDetachExecAgentCommand() *cobra.Command { func newDetachExecCommand() *cobra.Command { cmd := &cobra.Command{ Use: "exec", - Short: "Detach an Exec Session to a resource", - Long: `Detach an Exec Session to a Microservice or Agent.`, - Example: `iofogctl detach exec microservice AppName/MicroserviceName`, + Short: "Remove fog debug exec from an Agent", + Long: `Remove fog debug exec resources provisioned with attach exec agent.`, + Example: ex(`%[1]s detach exec agent AgentName`), } - // Add subcommands - cmd.AddCommand( - NewDetachExecMicroserviceCommand(), - newDetachExecAgentCommand(), - ) + cmd.AddCommand(newDetachExecAgentCommand()) return cmd } diff --git a/internal/cmd/detach_volume_mount.go b/internal/cmd/detach_volume_mount.go index c49111396..ddeb84aab 100644 --- a/internal/cmd/detach_volume_mount.go +++ b/internal/cmd/detach_volume_mount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,7 @@ func newDetachVolumeMountCommand() *cobra.Command { Use: "volume-mount NAME AGENT_NAME1 AGENT_NAME2", Short: "Detach a Volume Mount from existing Agents", Long: `Detach a Volume Mount from existing Agents.`, - Example: `iofogctl detach volume-mount NAME AGENT_NAME1 AGENT_NAME2`, + Example: ex(`%[1]s detach volume-mount NAME AGENT_NAME1 AGENT_NAME2`), Args: cobra.MinimumNArgs(2), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace of agent diff --git a/internal/cmd/disconnect.go b/internal/cmd/disconnect.go index 653ff8673..df2bdaeaf 100644 --- a/internal/cmd/disconnect.go +++ b/internal/cmd/disconnect.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -32,7 +19,7 @@ func newDisconnectCommand() *cobra.Command { This will remove all client-side information for this Namespace. The Namespace will itself be deleted. Use the connect command to reconnect after a disconnect. If you would like to uninstall the Control Plane and/or Agents, use the delete command instead.`, - Example: `iofogctl disconnect -n NAMESPACE`, + Example: ex(`%[1]s disconnect -n NAMESPACE`), Run: func(cmd *cobra.Command, args []string) { var err error opt.Namespace, err = cmd.Flags().GetString("namespace") diff --git a/internal/cmd/exec.go b/internal/cmd/exec.go index f9bfc0dee..ef69dc6d6 100644 --- a/internal/cmd/exec.go +++ b/internal/cmd/exec.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( diff --git a/internal/cmd/exec_agent.go b/internal/cmd/exec_agent.go index 0cd1e25d4..bbf47f021 100644 --- a/internal/cmd/exec_agent.go +++ b/internal/cmd/exec_agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -25,15 +12,18 @@ func newExecAgentCommand() *cobra.Command { } cmd := &cobra.Command{ - Use: "agent AgentName", - Short: "Connect to an Exec Session of an Agent", - Long: `Connect to an Exec Session of an Agent to interact with its container.`, - Example: `iofogctl exec agent AgentName`, - Args: cobra.ExactArgs(1), + Use: "agent AgentName [DEBUG_IMAGE]", + Short: "Open an interactive exec session on an Agent debug shell", + Long: `Open a WebSocket exec session to the Agent debug microservice. Provisions fog debug exec automatically when it is not already enabled.`, + Example: ex(`%[1]s exec agent AgentName +%[1]s exec agent AgentName ghcr.io/org/debug:latest`), + Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { - // Get resource type and name var err error opt.Name = args[0] + if len(args) > 1 { + opt.DebugImage = &args[1] + } opt.Namespace, err = cmd.Flags().GetString("namespace") util.Check(err) diff --git a/internal/cmd/exec_microservice.go b/internal/cmd/exec_microservice.go index e6170ecbe..3473a3aa4 100644 --- a/internal/cmd/exec_microservice.go +++ b/internal/cmd/exec_microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -26,9 +13,9 @@ func newExecMicroserviceCommand() *cobra.Command { cmd := &cobra.Command{ Use: "microservice AppName/MsvcName", - Short: "Connect to an Exec Session of a Microservice", - Long: `Connect to an Exec Session of a Microservice to interact with its container.`, - Example: `iofogctl exec microservice AppName/MicroserviceName`, + Short: "Open an interactive exec session to a Microservice", + Long: `Open a WebSocket exec session to a running Microservice. No attach step is required.`, + Example: ex(`%[1]s exec microservice AppName/MicroserviceName`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/generate_documentation.go b/internal/cmd/generate_documentation.go index 5da5e3c7c..50f28e96a 100644 --- a/internal/cmd/generate_documentation.go +++ b/internal/cmd/generate_documentation.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -33,31 +20,30 @@ func newGenerateDocumentationCommand(rootCmd *cobra.Command) *cobra.Command { cmd := &cobra.Command{ Use: "documentation TYPE", Hidden: true, - Short: "Generate iofogctl documentation", - Long: "Generate iofogctl documentation as markdown or man page", - Example: `iofogctl documentation md - iofogctl documentation man`, + Short: ex("Generate %[1]s documentation"), + Long: ex("Generate %[1]s documentation as markdown or man page"), + Example: ex(`%[1]s documentation md +%[1]s documentation man`), Args: cobra.ExactValidArgs(1), Run: func(cmd *cobra.Command, args []string) { if docDir == "" { docDir = home + "/.iofog/docs/" - err = os.MkdirAll(docDir, 0755) + err = os.MkdirAll(docDir, util.DirPerm) util.Check(err) } switch t := strings.ToLower(args[0]); t { case "md": - mdDir := path.Join(docDir, "md/") - err = os.MkdirAll(mdDir, 0755) + err = os.MkdirAll(docDir, util.DirPerm) util.Check(err) - err = doc.GenMarkdownTree(rootCmd, mdDir) + err = doc.GenMarkdownTree(rootCmd, docDir) util.Check(err) - util.PrintSuccess(fmt.Sprintf("markdown documentation generated at %s", mdDir)) + util.PrintSuccess(fmt.Sprintf("markdown documentation generated at %s", docDir)) case "man": manDir := path.Join(docDir, "man/") - err = os.MkdirAll(manDir, 0755) + err = os.MkdirAll(manDir, util.DirPerm) util.Check(err) header := &doc.GenManHeader{ - Title: "iofogctl", + Title: util.GetCliBinaryName(), Section: "1", } err := doc.GenManTree(rootCmd, header, manDir) diff --git a/internal/cmd/get.go b/internal/cmd/get.go index 122dff0cb..036e8c057 100644 --- a/internal/cmd/get.go +++ b/internal/cmd/get.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -28,7 +15,6 @@ func newGetCommand() *cobra.Command { "namespaces", "controllers", "agents", - "edge-resources", "application-templates", "applications", "system-applications", @@ -56,11 +42,10 @@ func newGetCommand() *cobra.Command { Long: `Get information of existing resources. Resources like Agents will require a working Controller in the namespace to display all information.`, - Example: `iofogctl get all + Example: ex(`%[1]s get all namespaces controllers agents - edge-resources application-templates applications system-applications @@ -80,7 +65,7 @@ Resources like Agents will require a working Controller in the namespace to disp nats-accounts nats-users nats-account-rules - nats-user-rules`, + nats-user-rules`), ValidArgs: validResources, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { diff --git a/internal/cmd/help.go b/internal/cmd/help.go new file mode 100644 index 000000000..e68b35be4 --- /dev/null +++ b/internal/cmd/help.go @@ -0,0 +1,13 @@ +package cmd + +import ( + "fmt" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +// ex formats Cobra help strings with the build-time CLI binary name as %[1]s. +func ex(format string, args ...any) string { + all := append([]any{util.GetCliBinaryName()}, args...) + return fmt.Sprintf(format, all...) +} diff --git a/internal/cmd/legacy.go b/internal/cmd/legacy.go deleted file mode 100644 index f2541fffc..000000000 --- a/internal/cmd/legacy.go +++ /dev/null @@ -1,173 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - "context" - "fmt" - - "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" - - "github.com/eclipse-iofog/iofogctl/internal/config" - rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" // GCP Auth pkg required - "k8s.io/client-go/tools/clientcmd" -) - -const ( - sshErrMsg = "legacy commands requires SSH access.\n%s %s SSH details are not available.\nUse `iofogctl configure --help` to find out how to add SSH details" -) - -func k8sExecute(kubeConfig, namespace, podSelector string, cliCmd, cmd []string) { - kubeConfig, err := util.FormatPath(kubeConfig) - util.Check(err) - // Connect to cluster - // Execute - conf, err := clientcmd.BuildConfigFromFlags("", kubeConfig) - util.Check(err) - // Instantiate Kubernetes client - clientset, err := kubernetes.NewForConfig(conf) - util.Check(err) - podList, err := clientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: podSelector}) - if err != nil { - return - } - podName := podList.Items[0].Name - kubeArgs := []string{"exec", podName, "-n", namespace, "--"} - kubeArgs = append(kubeArgs, cliCmd...) - kubeArgs = append(kubeArgs, cmd...) - kubectlCmd := "kubectl" - for _, kArg := range kubeArgs { - kubectlCmd = kubectlCmd + " " + kArg - } - util.PrintNotify("Cannot use legacy command against a Kubernetes Controller. Use the following command instead: \n\n " + kubectlCmd) -} - -func localExecute(container string, localCLI, localCmd []string) { - // Execute command - localContainerClient, err := install.NewLocalContainerClient() - util.Check(err) - cmd := append(localCLI, localCmd...) - result, err := localContainerClient.ExecuteCmd(container, cmd) - util.Check(err) - fmt.Print(result.StdOut) - if len(result.StdErr) > 0 { - util.PrintError(result.StdErr) - } -} - -func remoteExec(user, host, keyFile string, port int, cliCmd string, cmd []string) { - ssh, err := util.NewSecureShellClient(user, host, keyFile) - util.Check(err) - ssh.SetPort(port) - util.Check(ssh.Connect()) - defer util.Log(ssh.Disconnect) - - sshCmd := cliCmd - for _, arg := range cmd { - sshCmd = sshCmd + " " + arg - } - logs, err := ssh.Run(sshCmd) - util.Check(err) - fmt.Print(logs.String()) -} - -// NOTE: (Serge) This code will be discarded eventually. Keeping it one file. -func newLegacyCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "legacy resource NAME COMMAND ARGS...", - Short: "Execute commands using legacy CLI", - Long: `Execute commands using legacy Controller and Agent CLI. - -Legacy commands require SSH access to the corresponding Agent or Controller. - -Use the configure command to add SSH details to Agents and Controllers if necessary.`, - Example: `iofogctl legacy controller NAME COMMAND -iofogctl legacy agent NAME COMMAND`, - Args: cobra.MinimumNArgs(3), - Run: func(cmd *cobra.Command, args []string) { - // Get resource type arg - resource := args[0] - // Get resource name - name := args[1] - // Get namespace - namespace, err := cmd.Flags().GetString("namespace") - util.Check(err) - useDetached, err := cmd.Flags().GetBool("detached") - util.Check(err) - - ns, err := config.GetNamespace(namespace) - util.Check(err) - switch resource { - case "controller": - // Get config - controlPlane, err := ns.GetControlPlane() - util.Check(err) - baseController, err := controlPlane.GetController(name) - util.Check(err) - cliCommand := []string{"iofog-controller"} - switch controller := baseController.(type) { - case *rsc.KubernetesController: - k8sControlPlane, ok := controlPlane.(*rsc.KubernetesControlPlane) - if !ok { - util.Check(util.NewError("Could not convert Control Plane to Kubernetes Control Plane")) - } - util.Check(k8sControlPlane.ValidateKubeConfig()) - k8sExecute(k8sControlPlane.KubeConfig, namespace, "name=controller", cliCommand, args[2:]) - case *rsc.RemoteController: - if controller.ValidateSSH() != nil { - util.Check(fmt.Errorf(sshErrMsg, "Controller", controller.Name)) - } - remoteExec(controller.SSH.User, controller.Host, controller.SSH.KeyFile, controller.SSH.Port, "sudo iofog-controller", args[2:]) - case *rsc.LocalController: - localExecute(install.GetLocalContainerName("controller", false), cliCommand, args[2:]) - } - case "agent": - // Update local cache based on Controller - err := clientutil.SyncAgentInfo(namespace) - util.Check(err) - // Get config - var baseAgent rsc.Agent - if useDetached { - baseAgent, err = config.GetDetachedAgent(name) - } else { - baseAgent, err = ns.GetAgent(name) - } - util.Check(err) - switch agent := baseAgent.(type) { - case *rsc.LocalAgent: - localExecute(install.GetLocalContainerName("agent", false), []string{"iofog-agent"}, args[2:]) - return - case *rsc.RemoteAgent: - // SSH connect - if agent.ValidateSSH() != nil { - util.Check(fmt.Errorf(sshErrMsg, "Agent", agent.Name)) - } - remoteExec(agent.SSH.User, agent.Host, agent.SSH.KeyFile, agent.SSH.Port, "sudo iofog-agent", args[2:]) - } - default: - util.Check(util.NewInputError("Unknown legacy CLI " + resource)) - } - }, - } - - cmd.Flags().Bool("detached", false, pkg.flagDescDetached) - - return cmd -} diff --git a/internal/cmd/logs.go b/internal/cmd/logs.go index 4b94942a5..ba8e277db 100644 --- a/internal/cmd/logs.go +++ b/internal/cmd/logs.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,9 +11,9 @@ func newLogsCommand() *cobra.Command { Use: "logs RESOURCE NAME", Short: "Get log contents of deployed resource", Long: `Get log contents of deployed resource`, - Example: `iofogctl logs controller NAME + Example: ex(`%[1]s logs controller NAME agent NAME - microservice AppName/MsvcName`, + microservice AppName/MsvcName`), Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { // Get Resource type and name @@ -68,7 +55,7 @@ func newLogsCommand() *cobra.Command { } // Add flags for log tail configuration - cmd.Flags().Int("tail", 100, "Number of lines to tail (range: 1-10000)") + cmd.Flags().Int("tail", 100, "Number of lines to tail (range: 1-5000)") cmd.Flags().Bool("follow", true, "Follow log output") cmd.Flags().String("since", "", "Start time in ISO 8601 format (e.g., 2024-01-01T00:00:00Z)") cmd.Flags().String("until", "", "End time in ISO 8601 format (e.g., 2024-01-02T00:00:00Z)") diff --git a/internal/cmd/move.go b/internal/cmd/move.go index bcefe9739..e4e15ce25 100644 --- a/internal/cmd/move.go +++ b/internal/cmd/move.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( diff --git a/internal/cmd/move_agent.go b/internal/cmd/move_agent.go index 399cfe7c0..bf9b76b0b 100644 --- a/internal/cmd/move_agent.go +++ b/internal/cmd/move_agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -27,7 +14,7 @@ func newMoveAgentCommand() *cobra.Command { Use: "agent NAME DEST_NAMESPACE", Short: "Move an Agent to another Namespace", Long: `Move an Agent to another Namespace`, - Example: `iofogctl move agent NAME DEST_NAMESPACE`, + Example: ex(`%[1]s move agent NAME DEST_NAMESPACE`), Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { // Get args diff --git a/internal/cmd/move_microservice.go b/internal/cmd/move_microservice.go index 1182d9cbb..7d6c17c8d 100644 --- a/internal/cmd/move_microservice.go +++ b/internal/cmd/move_microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newMoveMicroserviceCommand() *cobra.Command { Use: "microservice NAME AGENT_NAME", Short: "Move a Microservice to another Agent in the same Namespace", Long: `Move a Microservice to another Agent in the same Namespace`, - Example: `iofogctl move microservice NAME AGENT_NAME`, + Example: ex(`%[1]s move microservice NAME AGENT_NAME`), Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace diff --git a/internal/cmd/nats.go b/internal/cmd/nats.go index 3df16a0ee..79acf5bbd 100644 --- a/internal/cmd/nats.go +++ b/internal/cmd/nats.go @@ -25,10 +25,10 @@ func newNatsCommand() *cobra.Command { Use: "nats", Short: "Manage NATS resources", Long: "Manage NATS-specific operations exposed by Controller APIs. Use get/describe/deploy/delete for CRUD-style NATS resources.", - Example: `iofogctl nats operator describe -iofogctl nats accounts ensure my-app --nats-rule default-account-rule -iofogctl nats users create my-app service-user -iofogctl nats users creds my-app service-user`, + Example: ex(`%[1]s nats operator describe +%[1]s nats accounts ensure my-app --nats-rule default-account-rule +%[1]s nats users create my-app service-user +%[1]s nats users creds my-app service-user`), } cmd.AddCommand( @@ -88,7 +88,7 @@ func newNatsAccountsCommand() *cobra.Command { Aliases: []string{"account"}, Short: "NATS account operations", Long: "NATS-specific account actions for applications.", - Example: `iofogctl nats accounts ensure my-application --nats-rule default-account-rule`, + Example: ex(`%[1]s nats accounts ensure my-application --nats-rule default-account-rule`), } cmd.AddCommand( newNatsAccountsEnsureCommand(), @@ -135,9 +135,9 @@ func newNatsUsersCommand() *cobra.Command { Aliases: []string{"user"}, Short: "NATS user operations", Long: "NATS-specific user actions such as create/delete and creds retrieval.", - Example: `iofogctl nats users create my-application service-user -iofogctl nats users creds my-application service-user -iofogctl nats users creds my-application service-user -o ./service-user.creds`, + Example: ex(`%[1]s nats users create my-application service-user +%[1]s nats users creds my-application service-user +%[1]s nats users creds my-application service-user -o ./service-user.creds`), } cmd.AddCommand( newNatsUsersCreateCommand(), diff --git a/internal/cmd/nats_rules_common.go b/internal/cmd/nats_rules_common.go index 5bef10406..c89169be8 100644 --- a/internal/cmd/nats_rules_common.go +++ b/internal/cmd/nats_rules_common.go @@ -31,7 +31,7 @@ func buildNatsRuleManifest(kind string, rule client.NatsRuleInfo) map[string]int delete(spec, "isSystem") return map[string]interface{}{ - "apiVersion": "iofog.org/v3", + "apiVersion": util.GetCliApiVersion(), "kind": kind, "metadata": map[string]interface{}{ "name": rule.Name, diff --git a/internal/cmd/pkg.go b/internal/cmd/pkg.go index edea218c7..64ddd3991 100644 --- a/internal/cmd/pkg.go +++ b/internal/cmd/pkg.go @@ -1,41 +1,21 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( "fmt" - "strings" ) var pkg struct { flagDescDetached string flagDescYaml string - succRename string succMove string } func init() { pkg.flagDescDetached = "Specify command is to run against detached resources" pkg.flagDescYaml = "YAML file containing specifications for ioFog resources to deploy" - pkg.succRename = "Successfully renamed %s %s to %s" pkg.succMove = "Successfully moved %s %s to %s %s" } -func getRenameSuccessMessage(resource, oldName, newName string) string { - return fmt.Sprintf(pkg.succRename, strings.Title(strings.ToLower(resource)), oldName, newName) -} - func getMoveSuccessMessage(resource, name, otherResource, otherName string) string { - return fmt.Sprintf(pkg.succRename, resource, name, otherResource, otherName) + return fmt.Sprintf(pkg.succMove, resource, name, otherResource, otherName) } diff --git a/internal/cmd/prune.go b/internal/cmd/prune.go index 49016c543..3a2057262 100644 --- a/internal/cmd/prune.go +++ b/internal/cmd/prune.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( diff --git a/internal/cmd/prune_agent.go b/internal/cmd/prune_agent.go index 2764dce81..e28407ecf 100644 --- a/internal/cmd/prune_agent.go +++ b/internal/cmd/prune_agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -24,7 +11,7 @@ func newPruneAgentCommand() *cobra.Command { Use: "agent NAME", Short: "Remove all dangling images from Agent", Long: `Remove all the images which are not used by existing containers on the specified Agent`, - Example: `iofogctl prune agent NAME`, + Example: ex(`%[1]s prune agent NAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get name and namespace of agent diff --git a/internal/cmd/rebuild.go b/internal/cmd/rebuild.go index 30b6e93c9..3a7b8401c 100644 --- a/internal/cmd/rebuild.go +++ b/internal/cmd/rebuild.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( diff --git a/internal/cmd/rebuild_microservice.go b/internal/cmd/rebuild_microservice.go index 27e101d18..8018976cb 100644 --- a/internal/cmd/rebuild_microservice.go +++ b/internal/cmd/rebuild_microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -25,7 +12,7 @@ func newRebuildMicroserviceCommand() *cobra.Command { Use: "microservice AppNAME/MsvcNAME", Short: "Rebuilds a microservice", Long: "Rebuilds a microservice", - Example: `iofogctl rebuild microservice AppNAME/MsvcNAME`, + Example: ex(`%[1]s rebuild microservice AppNAME/MsvcNAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error diff --git a/internal/cmd/rebuild_system_microservice.go b/internal/cmd/rebuild_system_microservice.go index 1375af8ad..785c5c07b 100644 --- a/internal/cmd/rebuild_system_microservice.go +++ b/internal/cmd/rebuild_system_microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -25,7 +12,7 @@ func newRebuildSystemMicroserviceCommand() *cobra.Command { Use: "system-microservice AppNAME/MsvcNAME", Short: "Rebuilds a system microservice", Long: "Rebuilds a system microservice", - Example: `iofogctl rebuild system-microservice AppNAME/MsvcNAME`, + Example: ex(`%[1]s rebuild system-microservice AppNAME/MsvcNAME`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error diff --git a/internal/cmd/reconcile.go b/internal/cmd/reconcile.go new file mode 100644 index 000000000..c7d7bbbe6 --- /dev/null +++ b/internal/cmd/reconcile.go @@ -0,0 +1,60 @@ +package cmd + +import ( + reconcileagent "github.com/eclipse-iofog/iofogctl/internal/reconcile/agent" + reconcileservice "github.com/eclipse-iofog/iofogctl/internal/reconcile/service" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newReconcileCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "reconcile", + Short: "Retry async platform provisioning for an agent or service", + Long: "Enqueue a manual platform reconcile and wait for provisioning to complete.", + } + + cmd.AddCommand( + newReconcileAgentCommand(), + newReconcileServiceCommand(), + ) + return cmd +} + +func newReconcileAgentCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "agent NAME", + Short: "Reconcile fog router/NATS platform for an agent", + Example: ex("%[1]s reconcile agent my-agent"), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + namespace, err := cmd.Flags().GetString("namespace") + util.Check(err) + + exe := reconcileagent.NewExecutor(namespace, name) + util.Check(exe.Execute()) + util.PrintSuccess("Successfully reconciled agent " + namespace + "/" + name) + }, + } + return cmd +} + +func newReconcileServiceCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "service NAME", + Short: "Reconcile service hub provisioning", + Example: ex("%[1]s reconcile service my-service"), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + namespace, err := cmd.Flags().GetString("namespace") + util.Check(err) + + exe := reconcileservice.NewExecutor(namespace, name) + util.Check(exe.Execute()) + util.PrintSuccess("Successfully reconciled service " + namespace + "/" + name) + }, + } + return cmd +} diff --git a/internal/cmd/rename.go b/internal/cmd/rename.go deleted file mode 100644 index bc29bbf35..000000000 --- a/internal/cmd/rename.go +++ /dev/null @@ -1,39 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - "github.com/spf13/cobra" -) - -func newRenameCommand() *cobra.Command { - // Instantiate command - cmd := &cobra.Command{ - Use: "rename", - Short: "Rename the iofog resources that are currently deployed", - Long: `Rename the iofog resources that are currently deployed`, - } - - // Add subcommands - cmd.AddCommand( - newRenameNamespaceCommand(), - newRenameControllerCommand(), - newRenameAgentCommand(), - newRenameApplicationCommand(), - newRenameMicroserviceCommand(), - newRenameEdgeResourceCommand(), - ) - - return cmd -} diff --git a/internal/cmd/rename_agent.go b/internal/cmd/rename_agent.go deleted file mode 100644 index a8ce89eef..000000000 --- a/internal/cmd/rename_agent.go +++ /dev/null @@ -1,49 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - rename "github.com/eclipse-iofog/iofogctl/internal/rename/agent" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" -) - -func newRenameAgentCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "agent NAME NEW_NAME", - Short: "Rename an Agent", - Long: `Rename an Agent`, - Example: `iofogctl rename agent NAME NEW_NAME`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - // Get name and the new name of agent - name := args[0] - newName := args[1] - namespace, err := cmd.Flags().GetString("namespace") - util.Check(err) - useDetached, err := cmd.Flags().GetBool("detached") - util.Check(err) - - // Get an executor for the command - err = rename.Execute(namespace, name, newName, useDetached) - util.Check(err) - - util.PrintSuccess(getRenameSuccessMessage("Agent", name, newName)) - }, - } - - cmd.Flags().Bool("detached", false, pkg.flagDescDetached) - - return cmd -} diff --git a/internal/cmd/rename_application.go b/internal/cmd/rename_application.go deleted file mode 100644 index cdf498fa4..000000000 --- a/internal/cmd/rename_application.go +++ /dev/null @@ -1,45 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - rename "github.com/eclipse-iofog/iofogctl/internal/rename/application" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" -) - -func newRenameApplicationCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "application NAME NEW_NAME", - Short: "Rename an Application", - Long: `Rename a Application`, - Example: `iofogctl rename application NAME NEW_NAME`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - // Get name and the new name of the application - name := args[0] - newName := args[1] - namespace, err := cmd.Flags().GetString("namespace") - util.Check(err) - - // Get an executor for the command - err = rename.Execute(namespace, name, newName) - util.Check(err) - - util.PrintSuccess(getRenameSuccessMessage("Application", name, newName)) - }, - } - - return cmd -} diff --git a/internal/cmd/rename_controller.go b/internal/cmd/rename_controller.go deleted file mode 100644 index 1e0dd37b9..000000000 --- a/internal/cmd/rename_controller.go +++ /dev/null @@ -1,45 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - rename "github.com/eclipse-iofog/iofogctl/internal/rename/controller" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" -) - -func newRenameControllerCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "controller NAME NEW_NAME", - Short: "Rename a Controller", - Long: `Rename a Controller`, - Example: `iofogctl rename controller NAME NEW_NAME`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - // Get name and the new name of Controller - name := args[0] - newName := args[1] - namespace, err := cmd.Flags().GetString("namespace") - util.Check(err) - - // Get an executor for the command - err = rename.Execute(namespace, name, newName) - util.Check(err) - - util.PrintSuccess(getRenameSuccessMessage("Controller", name, newName)) - }, - } - - return cmd -} diff --git a/internal/cmd/rename_edge_resource.go b/internal/cmd/rename_edge_resource.go deleted file mode 100644 index 1e2099a40..000000000 --- a/internal/cmd/rename_edge_resource.go +++ /dev/null @@ -1,45 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - rename "github.com/eclipse-iofog/iofogctl/internal/rename/edgeresource" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" -) - -func newRenameEdgeResourceCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "edge-resource NAME NEW_NAME", - Short: "Rename an Edge Resource", - Long: `Rename an Edge Resource`, - Example: `iofogctl rename edge-resource NAME NEW_NAME`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - // Get name and new name of the edgeResource - name := args[0] - newName := args[1] - namespace, err := cmd.Flags().GetString("namespace") - util.Check(err) - - // Get an executor for the command - err = rename.Execute(namespace, name, newName) - util.Check(err) - - util.PrintSuccess(getRenameSuccessMessage("Edge Resource", name, newName)) - }, - } - - return cmd -} diff --git a/internal/cmd/rename_microservice.go b/internal/cmd/rename_microservice.go deleted file mode 100644 index e7b361182..000000000 --- a/internal/cmd/rename_microservice.go +++ /dev/null @@ -1,45 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - rename "github.com/eclipse-iofog/iofogctl/internal/rename/microservice" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" -) - -func newRenameMicroserviceCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "microservice NAME NEW_NAME", - Short: "Rename a Microservice", - Long: `Rename a Microservice`, - Example: `iofogctl rename microservice NAME NEW_NAME`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - // Get name and new name of the microservice - name := args[0] - newName := args[1] - namespace, err := cmd.Flags().GetString("namespace") - util.Check(err) - - // Get an executor for the command - err = rename.Execute(namespace, name, newName) - util.Check(err) - - util.PrintSuccess(getRenameSuccessMessage("Microservice", name, newName)) - }, - } - - return cmd -} diff --git a/internal/cmd/rename_namespace.go b/internal/cmd/rename_namespace.go deleted file mode 100644 index c1641be09..000000000 --- a/internal/cmd/rename_namespace.go +++ /dev/null @@ -1,43 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package cmd - -import ( - rename "github.com/eclipse-iofog/iofogctl/internal/rename/namespace" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/spf13/cobra" -) - -func newRenameNamespaceCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "namespace NAME NEW_NAME", - Short: "Rename a Namespace", - Long: `Rename a Namespace`, - Example: `iofogctl rename namespace NAME NEW_NAME`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - // Get name and new name of the namespace - name := args[0] - newName := args[1] - - // Get an executor for the command - err := rename.Execute(name, newName) - util.Check(err) - - util.PrintSuccess(getRenameSuccessMessage("Namespace", name, newName)) - }, - } - - return cmd -} diff --git a/internal/cmd/rollback.go b/internal/cmd/rollback.go index 287d1f8de..671be648b 100644 --- a/internal/cmd/rollback.go +++ b/internal/cmd/rollback.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -30,7 +17,7 @@ func newRollbackCommand() *cobra.Command { Use: "rollback RESOURCE NAME", Short: "Rollback ioFog resources", Long: `Rollback ioFog resources to latest versions available.`, - Example: `iofogctl rollback agent NAME`, + Example: ex(`%[1]s rollback agent NAME`), Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name @@ -50,7 +37,7 @@ func newRollbackCommand() *cobra.Command { err = exe.Execute() util.Check(err) - util.PrintSuccess(fmt.Sprintf("Succesfully scheduled rollback for %s %s", strings.Title(opt.ResourceType), opt.Name)) + util.PrintSuccess(fmt.Sprintf("Successfully scheduled rollback for %s %s", strings.Title(opt.ResourceType), opt.Name)) }, } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index e67f56ea4..08bf61c0d 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -21,26 +8,44 @@ import ( "github.com/spf13/cobra" ) -const TitleHeader = " _ ____ __ __ \n" + +const iofogctlTitleHeader = " _ ____ __ __ \n" + " (_)___ / __/___ ____ _____/ /_/ / \n" + " / / __ \\/ /_/ __ \\/ __ `/ ___/ __/ / \n" + " / / /_/ / __/ /_/ / /_/ / /__/ /_/ / \n" + " /_/\\____/_/ \\____/\\__, /\\___/\\__/_/ \n" + " /____/ \n" -const TitleMessage = "iofogctl is the CLI for ioFog. Think of it as a mix between terraform and kubectl.\n" + +const iofogctlTitleMessage = "iofogctl is the CLI for ioFog. Think of it as a mix between terraform and kubectl.\n" + "\n" + "Use `iofogctl version` to display the current version.\n\n" +const potctlTitleHeader = "\n" + + "██████╗ ██████╗ ████████╗ ██████╗████████╗██╗ \n" + + "██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝╚══██╔══╝██║ \n" + + "██████╔╝██║ ██║ ██║ ██║ ██║ ██║ \n" + + "██╔═══╝ ██║ ██║ ██║ ██║ ██║ ██║ \n" + + "██║ ╚██████╔╝ ██║ ╚██████╗ ██║ ███████╗\n" + + "╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝\n" + +const potctlTitleMessage = "potctl is the CLI for Datasance PoT. Think of it as a mix between terraform and kubectl.\n" + + "\n" + + "Use `potctl version` to display the current version.\n\n" + func printHeader() { - util.PrintInfo(TitleHeader) - util.PrintInfo("\n") - util.PrintInfo(TitleMessage) + if util.GetCliBinaryName() == "potctl" { + util.PrintInfo(potctlTitleHeader) + util.PrintInfo("\n") + util.PrintInfo(potctlTitleMessage) + } else { + util.PrintInfo(iofogctlTitleHeader) + util.PrintInfo("\n") + util.PrintInfo(iofogctlTitleMessage) + } } func NewRootCommand() *cobra.Command { var cmd = &cobra.Command{ - Use: "iofogctl", + Use: util.GetCliBinaryName(), //Short: "ioFog Unified Command Line Interface", PreRun: func(cmd *cobra.Command, args []string) { printHeader() @@ -58,7 +63,7 @@ func NewRootCommand() *cobra.Command { cobra.OnInitialize(initialize) // Global flags - cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Toggle for displaying verbose output of iofogctl") + cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Toggle for displaying verbose output of "+util.GetCliBinaryName()) cmd.PersistentFlags().BoolVar(&debug, "debug", false, "Toggle for displaying verbose output of API clients (HTTP and SSH)") cmd.PersistentFlags().StringP("namespace", "n", config.GetDefaultNamespaceName(), "Namespace to execute respective command within") @@ -75,7 +80,6 @@ func NewRootCommand() *cobra.Command { newGetCommand(), newDescribeCommand(), newLogsCommand(), - newLegacyCommand(), newVersionCommand(), newBashCompleteCommand(cmd), newGenerateDocumentationCommand(cmd), @@ -84,7 +88,7 @@ func NewRootCommand() *cobra.Command { newStopCommand(), newMoveCommand(), newRebuildCommand(), - newRenameCommand(), + newReconcileCommand(), newDockerPruneCommand(), newUpgradeCommand(), newRollbackCommand(), @@ -113,7 +117,7 @@ func initialize() { }, }) client.SetVerbosity(debug) - install.SetVerbosity(verbose) + install.SetVerbosity(verbose || debug) util.SpinEnable(!verbose && !debug) util.SetDebug(debug) } diff --git a/internal/cmd/start.go b/internal/cmd/start.go index 61035f966..984ca683f 100644 --- a/internal/cmd/start.go +++ b/internal/cmd/start.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( diff --git a/internal/cmd/start_application.go b/internal/cmd/start_application.go index c3027b151..3bd1ff176 100644 --- a/internal/cmd/start_application.go +++ b/internal/cmd/start_application.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -25,7 +12,7 @@ func newStartApplicationCommand() *cobra.Command { Use: "application NAME", Short: "Starts an application", Long: "Starts an application", - Example: `iofogctl start application NAME`, + Example: ex(`%[1]s start application NAME`), Args: cobra.ExactValidArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error diff --git a/internal/cmd/start_microservice.go b/internal/cmd/start_microservice.go index 69cbee79d..168e1053f 100644 --- a/internal/cmd/start_microservice.go +++ b/internal/cmd/start_microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -25,7 +12,7 @@ func newStartMicroserviceCommand() *cobra.Command { Use: "microservice AppNAME/MsvcNAME", Short: "Starts an microservice", Long: "Starts an microservice", - Example: `iofogctl start microservice AppNAME/MsvcNAME`, + Example: ex(`%[1]s start microservice AppNAME/MsvcNAME`), Args: cobra.ExactValidArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error diff --git a/internal/cmd/stop.go b/internal/cmd/stop.go index 98b5bc60e..521184f2e 100644 --- a/internal/cmd/stop.go +++ b/internal/cmd/stop.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( diff --git a/internal/cmd/stop_application.go b/internal/cmd/stop_application.go index 12fd7e64e..3ae4c6228 100644 --- a/internal/cmd/stop_application.go +++ b/internal/cmd/stop_application.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -25,7 +12,7 @@ func newStopApplicationCommand() *cobra.Command { Use: "application NAME", Short: "Stop an application", Long: "Stop an application", - Example: `iofogctl stop application NAME`, + Example: ex(`%[1]s stop application NAME`), Args: cobra.ExactValidArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error diff --git a/internal/cmd/stop_microservice.go b/internal/cmd/stop_microservice.go index 9f351e2c4..67b14d8ff 100644 --- a/internal/cmd/stop_microservice.go +++ b/internal/cmd/stop_microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -25,7 +12,7 @@ func newStopMicroserviceCommand() *cobra.Command { Use: "microservice AppNAME/MsvcNAME", Short: "Stop an microservice", Long: "Stop an microservice", - Example: `iofogctl stop microservice AppNAME/MsvcNAME`, + Example: ex(`%[1]s stop microservice AppNAME/MsvcNAME`), Args: cobra.ExactValidArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error diff --git a/internal/cmd/upgrade.go b/internal/cmd/upgrade.go index 123ebc122..8c3ff1757 100644 --- a/internal/cmd/upgrade.go +++ b/internal/cmd/upgrade.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -30,7 +17,7 @@ func newUpgradeCommand() *cobra.Command { Use: "upgrade RESOURCE NAME", Short: "Upgrade ioFog resources", Long: `Upgrade ioFog resources to latest versions available.`, - Example: `iofogctl upgrade agent NAME`, + Example: ex(`%[1]s upgrade agent NAME`), Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name @@ -50,7 +37,7 @@ func newUpgradeCommand() *cobra.Command { err = exe.Execute() util.Check(err) - util.PrintSuccess(fmt.Sprintf("Succesfully scheduled upgrade for %s %s", strings.Title(opt.ResourceType), opt.Name)) + util.PrintSuccess(fmt.Sprintf("Successfully scheduled upgrade for %s %s", strings.Title(opt.ResourceType), opt.Name)) }, } diff --git a/internal/cmd/version.go b/internal/cmd/version.go index ab1a89f4e..fddf5f407 100644 --- a/internal/cmd/version.go +++ b/internal/cmd/version.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( @@ -27,15 +14,15 @@ func newVersionCommand() *cobra.Command { Run: func(cmd *cobra.Command, args []string) { ecnFlag, err := cmd.Flags().GetBool("ecn") util.Check(err) - util.PrintInfo("iofogctl - Copyright (C) 2023 Contributors to the Eclipse ioFog Project\n") + util.PrintInfo(fmt.Sprintf("%s - Copyright (C) 2026 Contributors\n", util.GetCliBinaryName())) _ = util.Print(util.GetVersion()) if ecnFlag { fmt.Println("") fmt.Println("controller@" + util.GetControllerVersion()) - fmt.Println("agent@" + util.GetAgentVersion()) + fmt.Println("edgelet@" + util.GetEdgeletVersion()) fmt.Println("") fmt.Println(util.GetControllerImage()) - fmt.Println(util.GetAgentImage()) + fmt.Println(util.GetEdgeletImage()) fmt.Println(util.GetOperatorImage()) fmt.Println(util.GetRouterImage()) } diff --git a/internal/cmd/view.go b/internal/cmd/view.go index d366979dd..02c4cbebf 100644 --- a/internal/cmd/view.go +++ b/internal/cmd/view.go @@ -1,27 +1,11 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package cmd import ( "fmt" - "net" - "net/url" "os" - "strings" "github.com/eclipse-iofog/iofogctl/internal/config" - "github.com/eclipse-iofog/iofogctl/pkg/iofog" + "github.com/eclipse-iofog/iofogctl/internal/resource" "github.com/eclipse-iofog/iofogctl/pkg/util" "github.com/pkg/browser" "github.com/spf13/cobra" @@ -30,53 +14,26 @@ import ( func newViewCommand() *cobra.Command { cmd := &cobra.Command{ Use: "view", - Short: "Open ECN Viewer", + Short: "Open EdgeOps Console", Run: func(cmd *cobra.Command, args []string) { - // Get Control Plane namespace, err := cmd.Flags().GetString("namespace") util.Check(err) ns, err := config.GetNamespace(namespace) util.Check(err) if len(ns.GetControllers()) == 0 { - util.PrintError("You must deploy a Control Plane to a namespace to see an ECN Viewer") + util.PrintError("You must deploy a Control Plane to a namespace to open the EdgeOps Console") os.Exit(1) } cp, err := ns.GetControlPlane() - cpEndpoint, err := cp.GetEndpoint() + util.Check(err) + consoleURL, err := resource.ResolveConsoleURL(cp) if err != nil { - util.PrintError("Failed to get Control Plane endpoint: " + err.Error()) + util.PrintError("Failed to resolve EdgeOps Console URL: " + err.Error()) os.Exit(1) } - ctrl := ns.GetControllers()[0] - var endpoint string - if cpEndpoint != "" { - endpoint = cpEndpoint - } else { - endpoint = ctrl.GetEndpoint() - } - URL, err := url.Parse(endpoint) - if err != nil || URL.Host == "" { - URL, err = url.Parse("//" + ctrl.GetEndpoint()) // Try to see if controllerEndpoint is an IP, in which case it needs to be pefixed by // - } - util.Check(err) - if URL.Scheme == "" { - URL.Scheme = "http" - } - host := "" - if strings.Contains(URL.Host, ":") { - host, _, err = net.SplitHostPort(URL.Host) - util.Check(err) - } else { - host = URL.Host - } - if util.IsLocalHost(host) { - host += ":" + iofog.ControllerHostECNViewerPortString - } - URL.Host = host - ecnViewer := URL.String() - if err := browser.OpenURL(ecnViewer); err != nil { - util.PrintInfo("To see the ECN Viewer, open your browser and go to:\n") - util.PrintInfo(fmt.Sprintf("%s\n", URL)) + if err := browser.OpenURL(consoleURL); err != nil { + util.PrintInfo("To open the EdgeOps Console, go to:\n") + util.PrintInfo(fmt.Sprintf("%s\n", consoleURL)) } }, } diff --git a/internal/config/config.go b/internal/config/config.go index 9f569d3bb..21fe9cbca 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package config import ( @@ -34,20 +21,29 @@ var ( namespaces map[string]*rsc.Namespace ) +var ( + apiVersionGroup string + LatestAPIVersion string +) + const ( - apiVersionGroup = "iofog.org" - latestVersion = "v3" - LatestAPIVersion = apiVersionGroup + "/" + latestVersion - defaultDirname = ".iofog/" + latestVersion - namespaceDirname = "namespaces/" - offlineImagesDirname = "offline-images" - airgapImagesDirname = "airgap-images" - defaultFilename = "config.yaml" - configV3 = "iofogctl/v3" - CurrentConfigVersion = configV3 - detachedNamespace = "_detached" + latestVersion = "v3" + defaultDirname = ".iofog/" + latestVersion + namespaceDirname = "namespaces/" + offlineImagesDirname = "offline-images" + airgapImagesDirname = "airgap-images" + airgapBinariesDirname = "airgap-binaries" + defaultFilename = "config.yaml" + configV3 = "iofogctl/v3" + CurrentConfigVersion = configV3 + detachedNamespace = "_detached" ) +func init() { + apiVersionGroup = util.GetCliCrdGroup() + LatestAPIVersion = util.GetCliApiVersion() +} + // Init initializes config, namespace and unmarshalls the files func Init(configFolderArg string) { namespaces = make(map[string]*rsc.Namespace) @@ -76,7 +72,7 @@ func Init(configFolderArg string) { // Check config file already exists if _, err := os.Stat(configFilename); os.IsNotExist(err) { - err = os.MkdirAll(configFolder, 0755) + err = os.MkdirAll(configFolder, util.DirPerm) util.Check(err) // Create default config file @@ -85,6 +81,8 @@ func Init(configFolderArg string) { util.Check(err) } + util.Check(migrateConfigPermissions(configFolder)) + // Unmarshall the config file confHeader := iofogctlConfig{} err = util.UnmarshalYAML(configFilename, &confHeader) @@ -100,7 +98,7 @@ func Init(configFolderArg string) { nsFile := getNamespaceFile(initNamespace) if _, err := os.Stat(nsFile); os.IsNotExist(err) { flush = true - err = os.MkdirAll(namespaceDirectory, 0755) + err = os.MkdirAll(namespaceDirectory, util.DirPerm) util.Check(err) // Create default namespace file @@ -195,7 +193,7 @@ func flushNamespaces() error { return err } // Overwrite the file - err = os.WriteFile(getNamespaceFile(ns.Name), marshal, 0644) + err = os.WriteFile(getNamespaceFile(ns.Name), marshal, util.FilePerm) if err != nil { return err } @@ -210,7 +208,7 @@ func flushShared() error { return nil } // Overwrite the file - err = os.WriteFile(configFilename, marshal, 0644) + err = os.WriteFile(configFilename, marshal, util.FilePerm) if err != nil { return nil } @@ -222,6 +220,13 @@ func Flush() error { return flushNamespaces() } +// ConfigFolder returns the initialized CLI config root (e.g. ~/.iofog/v3). +// +//nolint:revive // ConfigFolder is the established config package API name. +func ConfigFolder() string { + return configFolder +} + // GetOfflineImageNamespaceDir returns the directory path used to store OfflineImage artifacts for a namespace. func GetOfflineImageNamespaceDir(namespace string) string { return path.Join(configFolder, offlineImagesDirname, namespace) @@ -239,6 +244,20 @@ func GetOfflineImageCacheDir(namespace, resourceName, platform string) string { return path.Join(pathElems...) } +// GetAirgapBinaryCachePath returns the local cache file path for an edgelet release binary. +func GetAirgapBinaryCachePath(namespace, osName, archName string) string { + artifact, err := util.EdgeletBinaryArtifact(osName, archName) + if err != nil { + artifact = "edgelet-" + osName + "-" + archName + } + pathElems := []string{configFolder, airgapBinariesDirname} + if namespace != "" { + pathElems = append(pathElems, namespace) + } + pathElems = append(pathElems, artifact) + return path.Join(pathElems...) +} + // GetAirgapImageCacheDir returns the directory path for a specific airgap image (namespace, imageRef, platform). // Image ref and platform are sanitized for use in the path (e.g. / and : replaced with _). func GetAirgapImageCacheDir(namespace, imageRef, platform string) string { @@ -256,7 +275,7 @@ func GetAirgapImageCacheDir(namespace, imageRef, platform string) string { func ValidateHeader(header *Header) error { if header.APIVersion != LatestAPIVersion { - return util.NewInputError(fmt.Sprintf("Unsupported YAML API version %s.\nPlease use version %s. See https://iofog.org for specification details.", header.APIVersion, LatestAPIVersion)) + return util.NewInputError(fmt.Sprintf("Unsupported YAML API version %s.\nPlease use version %s. See %s for specification details.", header.APIVersion, LatestAPIVersion, util.GetCliDocsUrl())) } return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 000000000..6b2d76a58 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,20 @@ +package config + +import ( + "testing" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func TestLatestAPIVersionFromLdflag(t *testing.T) { + if LatestAPIVersion != util.GetCliApiVersion() { + t.Fatalf("LatestAPIVersion = %q, want %q from ldflag", LatestAPIVersion, util.GetCliApiVersion()) + } +} + +func TestConfigPathUsesIofogV3(t *testing.T) { + const want = ".iofog/v3" + if defaultDirname != want { + t.Fatalf("defaultDirname = %q, want %q", defaultDirname, want) + } +} diff --git a/internal/config/detached_agent.go b/internal/config/detached_agent.go index f94c661e1..87ce6996e 100644 --- a/internal/config/detached_agent.go +++ b/internal/config/detached_agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package config import ( @@ -70,18 +57,6 @@ func AddDetachedAgent(agent rsc.Agent) error { return ns.AddAgent(agent) } -func RenameDetachedAgent(oldName, newName string) error { - detachedAgent, err := GetDetachedAgent(oldName) - if err != nil { - return err - } - if err := DeleteDetachedAgent(oldName); err != nil { - return err - } - detachedAgent.SetName(newName) - return AddDetachedAgent(detachedAgent) -} - func DeleteDetachedAgent(name string) error { ns, err := getNamespace(detachedNamespace) if err != nil { diff --git a/internal/config/namespace.go b/internal/config/namespace.go index 1a95ea1ef..063264866 100644 --- a/internal/config/namespace.go +++ b/internal/config/namespace.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package config import ( @@ -118,7 +105,7 @@ func AddNamespace(name, created string) error { return err } // Overwrite the file - err = os.WriteFile(getNamespaceFile(name), marshal, 0644) + err = os.WriteFile(getNamespaceFile(name), marshal, util.FilePerm) if err != nil { return err } @@ -154,7 +141,7 @@ func UpdateUser(name, accessToken, refreshToken string) error { } // Write the updated YAML data back to the file - err = os.WriteFile(getNamespaceFile(name), marshal, 0644) + err = os.WriteFile(getNamespaceFile(name), marshal, util.FilePerm) if err != nil { return err // Error in writing to the file } @@ -185,21 +172,3 @@ func DeleteNamespace(name string) error { return nil } - -// RenameNamespace renames a namespace -func RenameNamespace(name, newName string) error { - ns, err := getNamespace(name) - if err != nil { - util.PrintError("Could not find namespace " + name) - return err - } - ns.Name = newName - if err := os.Rename(getNamespaceFile(name), getNamespaceFile(newName)); err != nil { - return err - } - if name == conf.DefaultNamespace { - return SetDefaultNamespace(newName) - } - - return nil -} diff --git a/internal/config/permissions.go b/internal/config/permissions.go new file mode 100644 index 000000000..60f213dbb --- /dev/null +++ b/internal/config/permissions.go @@ -0,0 +1,82 @@ +package config + +import ( + "io/fs" + "os" + "path/filepath" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +// migrateConfigPermissions tightens permissions on an existing CLI config tree. +func migrateConfigPermissions(root string) error { + if root == "" { + return nil + } + r, err := os.OpenRoot(root) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + defer r.Close() + + return migrateDirPermissions(r, ".") +} + +func migrateDirPermissions(r *os.Root, rel string) error { + if err := chmodRootEntry(r, rel); err != nil { + return err + } + entries, err := fs.ReadDir(r.FS(), rel) + if err != nil { + return err + } + for _, entry := range entries { + childRel := joinRel(rel, entry.Name()) + if err := chmodRootEntry(r, childRel); err != nil { + return err + } + if entry.IsDir() { + if err := migrateDirPermissions(r, childRel); err != nil { + return err + } + } + } + return nil +} + +func joinRel(parent, name string) string { + if parent == "." { + return name + } + return filepath.Join(parent, name) +} + +func chmodRootEntry(r *os.Root, rel string) error { + f, err := r.Open(filepath.ToSlash(rel)) + if err != nil { + return err + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return err + } + mode := info.Mode() + if mode&os.ModeSymlink != 0 { + return nil + } + target := os.FileMode(util.FilePerm) + if info.IsDir() { + target = os.FileMode(util.DirPerm) + } else if mode.Perm()&0111 != 0 { + target = os.FileMode(util.ExecPerm) + } + if mode.Perm()&0777 == target&0777 { + return nil + } + return f.Chmod(target) +} diff --git a/internal/config/types.go b/internal/config/types.go index 0f7d57f47..d5fbc6d1e 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -9,7 +9,6 @@ type Kind string const ( AgentConfigKind Kind = "AgentConfig" CatalogItemKind Kind = "CatalogItem" - EdgeResourceKind Kind = "EdgeResource" iofogctlConfigKind Kind = "iofogctlConfig" iofogctlNamespaceKind Kind = "Namespace" RegistryKind Kind = "Registry" diff --git a/internal/configure/agent.go b/internal/configure/agent.go index 51c3d0e53..ab18f6b0d 100644 --- a/internal/configure/agent.go +++ b/internal/configure/agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package configure import ( diff --git a/internal/configure/controller.go b/internal/configure/controller.go index 512e02c60..17bc4798c 100644 --- a/internal/configure/controller.go +++ b/internal/configure/controller.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package configure import ( diff --git a/internal/configure/controlplane.go b/internal/configure/controlplane.go index 4f0c88a21..7b8eda133 100644 --- a/internal/configure/controlplane.go +++ b/internal/configure/controlplane.go @@ -1,21 +1,9 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package configure import ( "github.com/eclipse-iofog/iofogctl/internal/config" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/internal/trust" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -23,9 +11,15 @@ type kubernetesConfig struct { kubeConfig string } +type caConfig struct { + caFile string + caB64 string +} + type controlPlaneExecutor struct { namespace string kubernetesConfig kubernetesConfig + caConfig caConfig name string remoteConfig remoteConfig } @@ -42,6 +36,10 @@ func newControlPlaneExecutor(opt *Options) *controlPlaneExecutor { kubernetesConfig: kubernetesConfig{ kubeConfig: opt.KubeConfig, }, + caConfig: caConfig{ + caFile: opt.CAFile, + caB64: opt.CAB64, + }, } } @@ -50,7 +48,11 @@ func (exe *controlPlaneExecutor) GetName() string { } func (exe *controlPlaneExecutor) Execute() error { - // Get config + caBase64, err := trust.NormalizeTrustCA(exe.caConfig.caFile, exe.caConfig.caB64) + if err != nil { + return err + } + ns, err := config.GetNamespace(exe.namespace) if err != nil { return err @@ -62,15 +64,48 @@ func (exe *controlPlaneExecutor) Execute() error { switch controlPlane := baseControlPlane.(type) { case *rsc.RemoteControlPlane: - return util.NewInputError("Cannot configure Remote Control Plane as if it is a Kubernetes Control Plane") + if exe.kubernetesConfig.kubeConfig != "" { + return util.NewInputError("Cannot edit kube config of a Remote Control Plane") + } + if (remoteConfig{}) != exe.remoteConfig { + return util.NewInputError("Cannot configure SSH settings on a Control Plane; use configure controller or configure agents") + } + if caBase64 == "" { + return util.NewInputError("Nothing to configure for Remote Control Plane") + } + if err := rsc.SetTrustCA(controlPlane, caBase64); err != nil { + return err + } + if err := trust.StoreCA(exe.namespace, caBase64); err != nil { + return err + } case *rsc.KubernetesControlPlane: if err := exe.kubernetesConfigure(controlPlane); err != nil { return err } + if caBase64 != "" { + if err := rsc.SetTrustCA(controlPlane, caBase64); err != nil { + return err + } + if err := trust.StoreCA(exe.namespace, caBase64); err != nil { + return err + } + } case *rsc.LocalControlPlane: - return util.NewInputError("Cannot configure a Local Control Plane") + if exe.kubernetesConfig.kubeConfig != "" || (remoteConfig{}) != exe.remoteConfig { + return util.NewInputError("Cannot configure kube or SSH settings on a Local Control Plane") + } + if caBase64 == "" { + return util.NewInputError("Nothing to configure for Local Control Plane") + } + if err := rsc.SetTrustCA(controlPlane, caBase64); err != nil { + return err + } + if err := trust.StoreCA(exe.namespace, caBase64); err != nil { + return err + } } ns.SetControlPlane(baseControlPlane) @@ -78,7 +113,6 @@ func (exe *controlPlaneExecutor) Execute() error { } func (exe *controlPlaneExecutor) kubernetesConfigure(controlPlane *rsc.KubernetesControlPlane) (err error) { - // Error if remoteConfig is passed if (remoteConfig{}) != exe.remoteConfig { return util.NewInputError("Cannot edit remote config of a Kubernetes Control Plane") } diff --git a/internal/configure/controlplane_test.go b/internal/configure/controlplane_test.go new file mode 100644 index 000000000..4b8c875b1 --- /dev/null +++ b/internal/configure/controlplane_test.go @@ -0,0 +1,97 @@ +package configure + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/eclipse-iofog/iofogctl/internal/config" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/internal/trust" +) + +func TestControlPlaneExecutor_configureCAFromFile(t *testing.T) { + dir := t.TempDir() + config.Init(dir) + + caPath := filepath.Join(dir, "ca.pem") + pemBytes := writeTestCAPEM(t, caPath) + + if err := config.AddNamespace("prod", "2020-01-01T00:00:00Z"); err != nil { + t.Fatal(err) + } + ns, err := config.GetNamespace("prod") + if err != nil { + t.Fatal(err) + } + cp := &rsc.KubernetesControlPlane{KubeConfig: "/tmp/kube"} + ns.SetControlPlane(cp) + if err := config.Flush(); err != nil { + t.Fatal(err) + } + + exe := newControlPlaneExecutor(&Options{ + Namespace: "prod", + CAFile: caPath, + KubeConfig: "/tmp/kube-new", + }) + if err := exe.Execute(); err != nil { + t.Fatal(err) + } + + stored, err := config.GetNamespace("prod") + if err != nil { + t.Fatal(err) + } + storedCP, err := stored.GetControlPlane() + if err != nil { + t.Fatal(err) + } + k8sCP, ok := storedCP.(*rsc.KubernetesControlPlane) + if !ok { + t.Fatal("expected kubernetes control plane") + } + wantCA := base64.StdEncoding.EncodeToString(pemBytes) + if k8sCP.CA != wantCA { + t.Fatalf("CA = %q want %q", k8sCP.CA, wantCA) + } + if k8sCP.KubeConfig != "/tmp/kube-new" { + t.Fatalf("KubeConfig = %q", k8sCP.KubeConfig) + } + if !trust.HasCA("prod") { + t.Fatal("expected namespace trust CA") + } +} + +func writeTestCAPEM(t *testing.T, path string) []byte { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test-ca"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + } + der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + if err := os.WriteFile(path, pemBytes, 0o600); err != nil { + t.Fatal(err) + } + return pemBytes +} diff --git a/internal/configure/default_namespace.go b/internal/configure/default_namespace.go index e28ebc3bb..f060c6dd8 100644 --- a/internal/configure/default_namespace.go +++ b/internal/configure/default_namespace.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package configure import ( diff --git a/internal/configure/factory.go b/internal/configure/factory.go index 624457f8f..53b5b169e 100644 --- a/internal/configure/factory.go +++ b/internal/configure/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package configure import ( @@ -27,6 +14,8 @@ type Options struct { User string Port int UseDetached bool + CAFile string + CAB64 string } var multipleResources = map[string]bool{ diff --git a/internal/configure/multiple.go b/internal/configure/multiple.go index 888876e3f..0f968703b 100644 --- a/internal/configure/multiple.go +++ b/internal/configure/multiple.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package configure import ( diff --git a/internal/connect/controlplane/connect.go b/internal/connect/controlplane/connect.go index 0c8ade5d1..cfd8a1222 100644 --- a/internal/connect/controlplane/connect.go +++ b/internal/connect/controlplane/connect.go @@ -1,32 +1,43 @@ package connectcontrolplane import ( - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "context" + + "github.com/eclipse-iofog/iofogctl/internal/auth" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/internal/trust" "github.com/eclipse-iofog/iofogctl/pkg/util" ) -func Connect(ctrlPlane rsc.ControlPlane, endpoint string, ns *rsc.Namespace) error { - // Connect to Controller - baseURL, err := util.GetBaseURL(endpoint) +// PrepareTrust validates --ca / --ca-b64 flags or spec.ca from YAML and persists trust for the namespace. +func PrepareTrust(namespace string, cp rsc.ControlPlane, caFile, caB64 string) error { + normalized, err := trust.NormalizeTrustCA(caFile, caB64) if err != nil { return err } + if normalized != "" { + if err := rsc.SetTrustCA(cp, normalized); err != nil { + return err + } + return trust.StoreCA(namespace, normalized) + } + if ca := rsc.GetTrustCA(cp); ca != "" { + return trust.StoreCA(namespace, ca) + } + return nil +} + +func Connect(ctrlPlane rsc.ControlPlane, endpoint, namespace string, ns *rsc.Namespace) error { + user := ctrlPlane.GetUser() util.SpinHandlePrompt() - ctrl, err := client.NewAndLogin(client.Options{BaseURL: baseURL}, ctrlPlane.GetUser().Email, ctrlPlane.GetUser().GetRawPassword()) + agents, err := auth.ConnectLogin(context.Background(), namespace, endpoint, "", user.Email, user.GetRawPassword()) if err != nil { return err } util.SpinHandlePromptComplete() - // Get Agents - listAgentsResponse, err := ctrl.ListAgents(client.ListAgentsRequest{}) - if err != nil { - return err - } - // Update Agents config - for idx := range listAgentsResponse.Agents { - agent := &listAgentsResponse.Agents[idx] + for idx := range agents { + agent := &agents[idx] agentConfig := rsc.RemoteAgent{ Name: agent.Name, UUID: agent.UUID, diff --git a/internal/connect/controlplane/k8s/k8s.go b/internal/connect/controlplane/k8s/k8s.go index 244b5f13c..1d46bfd42 100644 --- a/internal/connect/controlplane/k8s/k8s.go +++ b/internal/connect/controlplane/k8s/k8s.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package connectk8scontrolplane import ( @@ -26,12 +13,16 @@ import ( type kubernetesExecutor struct { controlPlane *rsc.KubernetesControlPlane namespace string + caFile string + caB64 string } -func newKubernetesExecutor(controlPlane *rsc.KubernetesControlPlane, namespace string) *kubernetesExecutor { +func newKubernetesExecutor(controlPlane *rsc.KubernetesControlPlane, namespace, caFile, caB64 string) *kubernetesExecutor { return &kubernetesExecutor{ controlPlane: controlPlane, namespace: namespace, + caFile: caFile, + caB64: caB64, } } @@ -39,7 +30,7 @@ func (exe *kubernetesExecutor) GetName() string { return "Kubernetes Control Plane" } -func NewManualExecutor(namespace, endpoint, kubeConfig, email, password string) (execute.Executor, error) { +func NewManualExecutor(namespace, endpoint, kubeConfig, email, password, caFile, caB64 string) (execute.Executor, error) { controlPlane := &rsc.KubernetesControlPlane{ IofogUser: rsc.IofogUser{Email: email, Password: password}, KubeConfig: kubeConfig, @@ -48,11 +39,10 @@ func NewManualExecutor(namespace, endpoint, kubeConfig, email, password string) if err := controlPlane.Sanitize(); err != nil { return nil, err } - return newKubernetesExecutor(controlPlane, namespace), nil + return newKubernetesExecutor(controlPlane, namespace, caFile, caB64), nil } -func NewExecutor(namespace, name string, yaml []byte, kind config.Kind) (execute.Executor, error) { - // Read the input file +func NewExecutor(namespace, name string, yaml []byte, kind config.Kind, caFile, caB64 string) (execute.Executor, error) { controlPlane, err := rsc.UnmarshallKubernetesControlPlane(yaml) if err != nil { return nil, err @@ -62,43 +52,37 @@ func NewExecutor(namespace, name string, yaml []byte, kind config.Kind) (execute return nil, err } - return newKubernetesExecutor(&controlPlane, namespace), nil + return newKubernetesExecutor(&controlPlane, namespace, caFile, caB64), nil } func (exe *kubernetesExecutor) Execute() (err error) { - // Instantiate Kubernetes cluster object k8s, err := install.NewKubernetes(exe.controlPlane.KubeConfig, exe.namespace) if err != nil { return } - // Set HTTPS configuration if present in the control plane if exe.controlPlane.Controller.Https != nil { k8s.SetHttpsEnabled(exe.controlPlane.Controller.Https) } - if exe.controlPlane.Controller.EcnViewerURL != "" { - viewerDns := true - k8s.SetIsViewerDns(&viewerDns) - } - - // Check the resources exist in K8s namespace if err = k8s.ExistsInNamespace(exe.namespace); err != nil { return } - // Get Controller endpoint endpoint, err := k8s.GetControllerEndpoint() if err != nil { return } - // Establish connection ns, err := config.GetNamespace(exe.namespace) if err != nil { return } - err = connectcontrolplane.Connect(exe.controlPlane, endpoint, ns) + if err := connectcontrolplane.PrepareTrust(exe.namespace, exe.controlPlane, exe.caFile, exe.caB64); err != nil { + return err + } + + err = connectcontrolplane.Connect(exe.controlPlane, endpoint, exe.namespace, ns) if err != nil { return } @@ -117,8 +101,13 @@ func (exe *kubernetesExecutor) Execute() (err error) { } } exe.controlPlane.Endpoint = endpoint + if exe.controlPlane.Controller.PublicUrl == "" { + exe.controlPlane.Controller.PublicUrl = endpoint + } + if err := rsc.BackfillConsoleURL(exe.controlPlane); err != nil { + return err + } - // Save changes ns.SetControlPlane(exe.controlPlane) return config.Flush() } @@ -133,7 +122,6 @@ func formatEndpoint(endpoint string) string { } func validate(controlPlane rsc.ControlPlane) (err error) { - // Validate user user := controlPlane.GetUser() if user.Email == "" { return util.NewInputError("To connect, Control Plane Iofog User must contain non-empty value in email field") diff --git a/internal/connect/controlplane/remote/fmt_test.go b/internal/connect/controlplane/remote/fmt_test.go index 9fd7b332a..6d39f69e1 100644 --- a/internal/connect/controlplane/remote/fmt_test.go +++ b/internal/connect/controlplane/remote/fmt_test.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package connectremotecontrolplane import ( diff --git a/internal/connect/controlplane/remote/remote.go b/internal/connect/controlplane/remote/remote.go index 388ad5f8b..d51e5ac27 100644 --- a/internal/connect/controlplane/remote/remote.go +++ b/internal/connect/controlplane/remote/remote.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package connectremotecontrolplane import ( @@ -28,9 +15,11 @@ import ( type remoteExecutor struct { controlPlane *rsc.RemoteControlPlane namespace string + caFile string + caB64 string } -func NewManualExecutor(namespace, name, endpoint, email, password string) (execute.Executor, error) { +func NewManualExecutor(namespace, name, endpoint, email, password, caFile, caB64 string) (execute.Executor, error) { fmtEndpoint, err := formatEndpoint(endpoint) if err != nil { return nil, err @@ -51,10 +40,10 @@ func NewManualExecutor(namespace, name, endpoint, email, password string) (execu }, } - return newRemoteExecutor(controlPlane, namespace), nil + return newRemoteExecutor(controlPlane, namespace, caFile, caB64), nil } -func NewExecutor(namespace, name string, yaml []byte, kind config.Kind) (execute.Executor, error) { +func NewExecutor(namespace, name string, yaml []byte, kind config.Kind, caFile, caB64 string) (execute.Executor, error) { // Read the input file controlPlane, err := rsc.UnmarshallRemoteControlPlane(yaml) if err != nil { @@ -87,13 +76,15 @@ func NewExecutor(namespace, name string, yaml []byte, kind config.Kind) (execute } } - return newRemoteExecutor(&controlPlane, namespace), nil + return newRemoteExecutor(&controlPlane, namespace, caFile, caB64), nil } -func newRemoteExecutor(controlPlane *rsc.RemoteControlPlane, namespace string) *remoteExecutor { +func newRemoteExecutor(controlPlane *rsc.RemoteControlPlane, namespace, caFile, caB64 string) *remoteExecutor { r := &remoteExecutor{ controlPlane: controlPlane, namespace: namespace, + caFile: caFile, + caB64: caB64, } return r } @@ -116,7 +107,10 @@ func (exe *remoteExecutor) Execute() (err error) { if err != nil { return err } - err = connectcontrolplane.Connect(exe.controlPlane, endpoint, ns) + if err := connectcontrolplane.PrepareTrust(exe.namespace, exe.controlPlane, exe.caFile, exe.caB64); err != nil { + return err + } + err = connectcontrolplane.Connect(exe.controlPlane, endpoint, exe.namespace, ns) if err != nil { return err } diff --git a/internal/connect/execute.go b/internal/connect/execute.go index 21dcf1308..4fa71890a 100644 --- a/internal/connect/execute.go +++ b/internal/connect/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package connect import ( @@ -36,6 +23,8 @@ type Options struct { IofogUserPass string Generate bool Base64Encoded bool + CAFile string + CAB64 string } var kindOrder = []config.Kind{ @@ -43,13 +32,15 @@ var kindOrder = []config.Kind{ config.RemoteControlPlaneKind, } -var kindHandlers = map[config.Kind]func(*execute.KindHandlerOpt) (execute.Executor, error){ - config.KubernetesControlPlaneKind: func(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { - return connectk8scontrolplane.NewExecutor(opt.Namespace, opt.Name, opt.YAML, config.KubernetesControlPlaneKind) - }, - config.RemoteControlPlaneKind: func(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { - return connectremotecontrolplane.NewExecutor(opt.Namespace, opt.Name, opt.YAML, config.RemoteControlPlaneKind) - }, +func buildKindHandlers(caFile, caB64 string) map[config.Kind]func(*execute.KindHandlerOpt) (execute.Executor, error) { + return map[config.Kind]func(*execute.KindHandlerOpt) (execute.Executor, error){ + config.KubernetesControlPlaneKind: func(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { + return connectk8scontrolplane.NewExecutor(opt.Namespace, opt.Name, opt.YAML, config.KubernetesControlPlaneKind, caFile, caB64) + }, + config.RemoteControlPlaneKind: func(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { + return connectremotecontrolplane.NewExecutor(opt.Namespace, opt.Name, opt.YAML, config.RemoteControlPlaneKind, caFile, caB64) + }, + } } func Execute(opt *Options) error { @@ -91,7 +82,7 @@ func Execute(opt *Options) error { defer config.Flush() if opt.InputFile != "" { - return executeWithYAML(opt.InputFile, opt.Namespace) + return executeWithYAML(opt.InputFile, opt.Namespace, opt.CAFile, opt.CAB64) } return manualExecute(opt) } @@ -104,12 +95,12 @@ func manualExecute(opt *Options) (err error) { // K8s or Remote var exe execute.Executor if opt.KubeConfig != "" { - exe, err = connectk8scontrolplane.NewManualExecutor(opt.Namespace, opt.ControllerEndpoint, opt.KubeConfig, opt.IofogUserEmail, opt.IofogUserPass) + exe, err = connectk8scontrolplane.NewManualExecutor(opt.Namespace, opt.ControllerEndpoint, opt.KubeConfig, opt.IofogUserEmail, opt.IofogUserPass, opt.CAFile, opt.CAB64) if err != nil { return err } } else { - exe, err = connectremotecontrolplane.NewManualExecutor(opt.Namespace, opt.ControllerName, opt.ControllerEndpoint, opt.IofogUserEmail, opt.IofogUserPass) + exe, err = connectremotecontrolplane.NewManualExecutor(opt.Namespace, opt.ControllerName, opt.ControllerEndpoint, opt.IofogUserEmail, opt.IofogUserPass, opt.CAFile, opt.CAB64) if err != nil { return err } @@ -122,8 +113,9 @@ func manualExecute(opt *Options) (err error) { return nil } -func executeWithYAML(yamlFile, namespace string) error { - executorsMap, err := execute.GetExecutorsFromYAML(yamlFile, namespace, kindHandlers) +func executeWithYAML(yamlFile, namespace, caFile, caB64 string) error { + handlers := buildKindHandlers(caFile, caB64) + executorsMap, err := execute.GetExecutorsFromYAML(yamlFile, namespace, handlers, false) if err != nil { return err } diff --git a/internal/create/namespace/namespace.go b/internal/create/namespace/namespace.go index b78b9e27f..a8668072a 100644 --- a/internal/create/namespace/namespace.go +++ b/internal/create/namespace/namespace.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package createnamespace import ( diff --git a/internal/delete/agent/execute.go b/internal/delete/agent/execute.go index d8210aa09..b7b37a7f5 100644 --- a/internal/delete/agent/execute.go +++ b/internal/delete/agent/execute.go @@ -1,25 +1,15 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteagent import ( "fmt" + "strings" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/config" "github.com/eclipse-iofog/iofogctl/internal/execute" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -69,6 +59,13 @@ func (exe executor) Execute() (err error) { baseAgent, err = ns.GetAgent(exe.name) if err != nil { + if util.IsNotFoundError(err) { + clientutil.InvalidateAgentCache(exe.namespace) + backendAgents, backendErr := clientutil.GetBackendAgents(exe.namespace) + if backendErr == nil && !agentListedInBackend(exe.name, backendAgents) { + return nil + } + } return err } @@ -79,27 +76,45 @@ func (exe executor) Execute() (err error) { } } - // Remove from Controller switch agent := baseAgent.(type) { case *rsc.LocalAgent: - if err = exe.deleteLocalContainer(); err != nil { - util.PrintInfo(fmt.Sprintf("Could not remove Agent container %s. Error: %s\n", agent.GetHost(), err.Error())) + install.Verbose("Deprovisioning edgelet on local agent " + agent.GetName()) + if err = exe.deprovisionLocalEdgelet(agent); err != nil { + util.PrintInfo(fmt.Sprintf("Could not deprovision Agent on the local host %s. Error: %s\n", agent.GetHost(), err.Error())) } case *rsc.RemoteAgent: - if err = exe.deleteRemoteAgent(agent); err != nil { - util.PrintInfo(fmt.Sprintf("Could not remove Agent from the remote host %s. Error: %s\n", agent.GetHost(), err.Error())) + install.Verbose("Deprovisioning edgelet on remote agent " + agent.GetName()) + if err = exe.deprovisionRemoteEdgelet(agent); err != nil { + util.PrintInfo(fmt.Sprintf("Could not deprovision Agent on the remote host %s. Error: %s\n", agent.GetHost(), err.Error())) } } - // Try to get a Controller client to talk to the REST API + // Delete from Controller while it is still reachable after deprovision. ctrl, err := clientutil.NewControllerClient(exe.namespace) if err != nil { util.PrintInfo(fmt.Sprintf("Could not delete Agent %s from the Controller. Error: %s\n", exe.name, err.Error())) + } else if baseAgent.GetUUID() != "" { + if err := ctrl.DeleteAgent(baseAgent.GetUUID()); err != nil && !isControllerAgentNotFound(err) { + return err + } } - // Perform deletion of Agent through Controller - if err := ctrl.DeleteAgent(baseAgent.GetUUID()); err != nil { - return err + + clientutil.InvalidateAgentCache(exe.namespace) + + // Remove edgelet from host + switch agent := baseAgent.(type) { + case *rsc.LocalAgent: + install.Verbose("Uninstalling edgelet from local agent " + agent.GetName()) + if err = exe.uninstallLocalEdgelet(agent); err != nil { + util.PrintInfo(fmt.Sprintf("Could not remove Agent from the local host %s. Error: %s\n", agent.GetHost(), err.Error())) + } + case *rsc.RemoteAgent: + install.Verbose("Uninstalling edgelet from remote agent " + agent.GetName()) + if err = exe.uninstallRemoteEdgelet(agent); err != nil { + util.PrintInfo(fmt.Sprintf("Could not remove Agent from the remote host %s. Error: %s\n", agent.GetHost(), err.Error())) + } } + if err := ns.DeleteAgent(baseAgent.GetName()); err != nil { return err } @@ -135,6 +150,26 @@ func (exe executor) Execute() (err error) { return config.Flush() } +func agentListedInBackend(name string, agents []client.AgentInfo) bool { + for idx := range agents { + if agents[idx].Name == name { + return true + } + } + return false +} + +func isControllerAgentNotFound(err error) bool { + if err == nil { + return false + } + if util.IsNotFoundError(err) { + return true + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "notfound") || strings.Contains(msg, "not found") +} + func (exe executor) checkMicroservices(agentName, agentUUID string) (err error) { // Try to get a Controller client to talk to the REST API ctrl, err := clientutil.NewControllerClient(exe.namespace) diff --git a/internal/delete/agent/local.go b/internal/delete/agent/local.go index 8853c7672..f2a38165e 100644 --- a/internal/delete/agent/local.go +++ b/internal/delete/agent/local.go @@ -1,39 +1,45 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteagent import ( "fmt" "strings" - "github.com/eclipse-iofog/iofogctl/pkg/util" - + deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" ) -func (exe executor) deleteLocalContainer() error { - client, err := install.NewLocalContainerClient() +func (exe executor) newLocalEdgelet(agent *rsc.LocalAgent) (*install.LocalEdgelet, error) { + cfg := deployairgap.EdgeletInstallConfig(deployairgap.LocalEdgeletHostOS(), agent.Config, agent.Package) + return install.NewLocalEdgelet(agent.Name, agent.UUID, cfg) +} + +func (exe executor) deprovisionLocalEdgelet(agent *rsc.LocalAgent) error { + edgelet, err := exe.newLocalEdgelet(agent) if err != nil { return err } + if err := edgelet.Deprovision(); err != nil { + util.PrintNotify(fmt.Sprintf("Could not deprovision edgelet on local host: %v", err)) + } + return nil +} - // Clean agent containers (normal and system) - if errClean := client.CleanContainer(install.GetLocalContainerName("agent", false)); errClean != nil { - util.PrintNotify(fmt.Sprintf("Could not clean Agent container: %v", errClean)) +func (exe executor) uninstallLocalEdgelet(agent *rsc.LocalAgent) error { + cfg := deployairgap.EdgeletInstallConfig(deployairgap.LocalEdgeletHostOS(), agent.Config, agent.Package) + edgelet, err := install.NewLocalEdgelet(agent.Name, agent.UUID, cfg) + if err != nil { + return err + } + if err := edgelet.Uninstall(true); err != nil { + util.PrintNotify(fmt.Sprintf("Could not remove edgelet from local host: %v", err)) } - // Clean microservices + client, err := install.NewLocalContainerClientFromEdgeletCfg(cfg) + if err != nil { + return err + } containers, err := client.ListContainers() if err != nil { return err @@ -48,6 +54,12 @@ func (exe executor) deleteLocalContainer() error { } } } - return nil } + +func (exe executor) deleteLocalEdgelet(agent *rsc.LocalAgent) error { + if err := exe.deprovisionLocalEdgelet(agent); err != nil { + return err + } + return exe.uninstallLocalEdgelet(agent) +} diff --git a/internal/delete/agent/remote.go b/internal/delete/agent/remote.go index b79fe8958..f6a092480 100644 --- a/internal/delete/agent/remote.go +++ b/internal/delete/agent/remote.go @@ -1,43 +1,60 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteagent import ( "fmt" + deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) -func (exe executor) deleteRemoteAgent(agent *rsc.RemoteAgent) error { - // Stop and remove the Agent process on remote server +func (exe executor) newRemoteEdgelet(agent *rsc.RemoteAgent) (*install.RemoteEdgelet, error) { + cfg := deployairgap.EdgeletInstallConfig("linux", agent.Config, agent.Package) + return install.NewRemoteEdgelet( + agent.SSH.User, + agent.Host, + agent.SSH.Port, + agent.SSH.KeyFile, + agent.Name, + agent.UUID, + cfg, + ) +} + +func (exe executor) deprovisionRemoteEdgelet(agent *rsc.RemoteAgent) error { if agent.ValidateSSH() != nil { - util.PrintNotify("Could not stop daemon for Agent " + agent.Name + ". SSH details missing from local cofiguration. Use configure command to add SSH details.") - } else { - sshAgent, err := install.NewRemoteAgent(agent.SSH.User, - agent.Host, - agent.SSH.Port, - agent.SSH.KeyFile, - agent.Name, - agent.UUID) - if err != nil { - return err - } - if err := sshAgent.Uninstall(); err != nil { - util.PrintNotify(fmt.Sprintf("Failed to stop daemon on Agent %s. %s", agent.Name, err.Error())) - } + util.PrintNotify("Could not deprovision daemon for Agent " + agent.Name + ". SSH details missing from local configuration. Use configure command to add SSH details.") + return nil + } + edgelet, err := exe.newRemoteEdgelet(agent) + if err != nil { + return err + } + if err := edgelet.Deprovision(); err != nil { + util.PrintNotify(fmt.Sprintf("Could not deprovision edgelet on Agent %s: %v", agent.Name, err)) } return nil } + +func (exe executor) uninstallRemoteEdgelet(agent *rsc.RemoteAgent) error { + if agent.ValidateSSH() != nil { + util.PrintNotify("Could not stop daemon for Agent " + agent.Name + ". SSH details missing from local configuration. Use configure command to add SSH details.") + return nil + } + edgelet, err := exe.newRemoteEdgelet(agent) + if err != nil { + return err + } + if err := edgelet.Uninstall(true); err != nil { + util.PrintNotify(fmt.Sprintf("Failed to stop daemon on Agent %s. %s", agent.Name, err.Error())) + } + return nil +} + +func (exe executor) deleteRemoteAgent(agent *rsc.RemoteAgent) error { + if err := exe.deprovisionRemoteEdgelet(agent); err != nil { + return err + } + return exe.uninstallRemoteEdgelet(agent) +} diff --git a/internal/delete/agents/agents.go b/internal/delete/agents/agents.go new file mode 100644 index 000000000..7b9c2d836 --- /dev/null +++ b/internal/delete/agents/agents.go @@ -0,0 +1,61 @@ +package deleteagents + +import ( + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" +) + +type DeleteTarget struct { + Name string + Force bool +} + +func CollectDeleteTargets(ns *rsc.Namespace, namespace string, force bool, excludeNames []string) ([]DeleteTarget, error) { + if err := clientutil.SyncAgentInfo(namespace); err != nil && !rsc.IsNoControlPlaneError(err) { + return nil, err + } + + excluded := make(map[string]bool, len(excludeNames)) + for _, name := range excludeNames { + excluded[name] = true + } + + seen := make(map[string]bool) + systemAgents := make(map[string]bool) + targets := make([]DeleteTarget, 0) + + for _, agent := range ns.GetAgents() { + if seen[agent.GetName()] || excluded[agent.GetName()] { + continue + } + seen[agent.GetName()] = true + if cfg := agent.GetConfig(); cfg != nil && cfg.IsSystem != nil && *cfg.IsSystem { + systemAgents[agent.GetName()] = true + } + targets = append(targets, DeleteTarget{Name: agent.GetName()}) + } + + backendAgents, err := clientutil.GetBackendAgents(namespace) + if err != nil { + return targets, nil + } + for idx := range backendAgents { + agent := &backendAgents[idx] + if excluded[agent.Name] { + continue + } + if agent.IsSystem { + systemAgents[agent.Name] = true + } + if seen[agent.Name] { + continue + } + seen[agent.Name] = true + targets = append(targets, DeleteTarget{Name: agent.Name}) + } + + for i := range targets { + targets[i].Force = force || systemAgents[targets[i].Name] + } + return targets, nil +} diff --git a/internal/delete/all/all.go b/internal/delete/all/all.go index 760606c3d..c4f61129b 100644 --- a/internal/delete/all/all.go +++ b/internal/delete/all/all.go @@ -1,29 +1,19 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteall import ( "github.com/eclipse-iofog/iofogctl/internal/config" deleteagent "github.com/eclipse-iofog/iofogctl/internal/delete/agent" + deleteagents "github.com/eclipse-iofog/iofogctl/internal/delete/agents" deletecontrolplane "github.com/eclipse-iofog/iofogctl/internal/delete/controlplane" + deletelocalcontrolplane "github.com/eclipse-iofog/iofogctl/internal/delete/controlplane/local" deletevolume "github.com/eclipse-iofog/iofogctl/internal/delete/volume" "github.com/eclipse-iofog/iofogctl/internal/execute" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" "github.com/eclipse-iofog/iofogctl/pkg/util" ) -func Execute(namespace string, useDetached, force bool) error { +func Execute(namespace string, useDetached, force, deleteNamespace bool) error { // Make sure to update config despite failure defer config.Flush() @@ -51,31 +41,41 @@ func Execute(namespace string, useDetached, force bool) error { if !useDetached { // Delete applications - util.SpinStart("Deleting Flows") + util.SpinStart("Deleting Applications") clt, err := clientutil.NewControllerClient(namespace) if err != nil { return err } - flows, err := clt.GetAllFlows() + applications, err := clt.GetAllApplications() if err != nil { return err } - for _, flow := range flows.Flows { - if err := clt.DeleteFlow(flow.ID); err != nil { + for _, application := range applications.Applications { + if err := clt.DeleteApplication(application.Name); err != nil { return err } } } - // Delete Agents - if len(ns.GetAgents()) > 0 { + // Delete non-control-plane agents first while the controller is still reachable. + var excludeAgentNames []string + if cp, cpErr := ns.GetControlPlane(); cpErr == nil { + if localCP, ok := cp.(*rsc.LocalControlPlane); ok && localCP.SystemAgent != nil { + excludeAgentNames = []string{deletelocalcontrolplane.ControlPlaneSystemAgentName(localCP)} + } + } + agentTargets, err := deleteagents.CollectDeleteTargets(ns, namespace, force, excludeAgentNames) + if err != nil { + return err + } + if len(agentTargets) > 0 { util.SpinStart("Deleting Agents") var executors []execute.Executor - for _, agent := range ns.GetAgents() { - exe, err := deleteagent.NewExecutor(namespace, agent.GetName(), useDetached, force) + for _, target := range agentTargets { + exe, err := deleteagent.NewExecutor(namespace, target.Name, useDetached, target.Force) if err != nil { return err } @@ -89,7 +89,7 @@ func Execute(namespace string, useDetached, force bool) error { if !useDetached { // Delete Controllers util.SpinStart("Deleting Control Plane ") - exe, err := deletecontrolplane.NewExecutor(namespace) + exe, err := deletecontrolplane.NewExecutor(namespace, deleteNamespace) if err != nil { return err } diff --git a/internal/delete/application/execute.go b/internal/delete/application/execute.go index 5c9a4c09d..9e5acbfc9 100644 --- a/internal/delete/application/execute.go +++ b/internal/delete/application/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteapplication import ( diff --git a/internal/delete/application/legacy.go b/internal/delete/application/legacy.go index 0b1386a8f..a59d3b0ca 100644 --- a/internal/delete/application/legacy.go +++ b/internal/delete/application/legacy.go @@ -1,24 +1,11 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteapplication func (exe *Executor) initLegacy() (err error) { - flow, err := exe.client.GetFlowByName(exe.name) + application, err := exe.client.GetApplicationByName(exe.name) if err != nil { return } - exe.flow = flow + exe.application = application return } @@ -28,8 +15,8 @@ func (exe *Executor) deleteLegacy() (err error) { return } - // Delete flow - if err = exe.client.DeleteFlow(exe.flow.ID); err != nil { + // Delete application + if err = exe.client.DeleteApplication(exe.application.Name); err != nil { return } return diff --git a/internal/delete/application/remote.go b/internal/delete/application/remote.go index 3d157c546..907b230e9 100644 --- a/internal/delete/application/remote.go +++ b/internal/delete/application/remote.go @@ -1,19 +1,8 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteapplication import ( + "errors" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/execute" clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" @@ -21,10 +10,10 @@ import ( ) type Executor struct { - namespace string - name string - client *client.Client - flow *client.FlowInfo + namespace string + name string + client *client.Client + application *client.ApplicationInfo } func NewExecutor(namespace, name string) (execute.Executor, error) { @@ -49,7 +38,7 @@ func (exe *Executor) init() (err error) { return } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() (err error) { util.SpinStart("Deleting Application") if err := exe.init(); err != nil { @@ -58,7 +47,8 @@ func (exe *Executor) Execute() (err error) { err = exe.client.DeleteApplication(exe.name) // If notfound error, try legacy - if _, ok := err.(*client.NotFoundError); ok { + notFoundError := &client.NotFoundError{} + if errors.As(err, ¬FoundError) { return exe.deleteLegacy() } return err diff --git a/internal/delete/catalogitem/catalog_item.go b/internal/delete/catalogitem/catalog_item.go index f5cb69008..e7517c558 100644 --- a/internal/delete/catalogitem/catalog_item.go +++ b/internal/delete/catalogitem/catalog_item.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletecatalogitem import ( @@ -38,7 +25,7 @@ func (exe *Executor) GetName() string { return exe.name } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() error { util.SpinStart("Deleting Catalog item") // Init remote resources diff --git a/internal/delete/certificate/certificate.go b/internal/delete/certificate/certificate.go index b1e93d084..aa97999f8 100644 --- a/internal/delete/certificate/certificate.go +++ b/internal/delete/certificate/certificate.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletecertificate import ( @@ -43,7 +30,7 @@ func (exe *Executor) GetName() string { return exe.name } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() error { util.SpinStart("Deleting Certificate") // Init remote resources diff --git a/internal/delete/configmap/config_map.go b/internal/delete/configmap/config_map.go index 75149e179..5c1955430 100644 --- a/internal/delete/configmap/config_map.go +++ b/internal/delete/configmap/config_map.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteconfigmap import ( @@ -38,7 +25,7 @@ func (exe *Executor) GetName() string { return exe.name } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() error { util.SpinStart("Deleting ConfigMap") // Init remote resources diff --git a/internal/delete/controller/execute.go b/internal/delete/controller/execute.go index 01773ec13..faa7f5b5a 100644 --- a/internal/delete/controller/execute.go +++ b/internal/delete/controller/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletecontroller import ( diff --git a/internal/delete/controller/factory.go b/internal/delete/controller/factory.go index 257e4f322..83347f490 100644 --- a/internal/delete/controller/factory.go +++ b/internal/delete/controller/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletecontroller import ( diff --git a/internal/delete/controller/local.go b/internal/delete/controller/local.go index a1d7fcce4..d1cb1251f 100644 --- a/internal/delete/controller/local.go +++ b/internal/delete/controller/local.go @@ -1,42 +1,22 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletecontroller import ( - "fmt" - "github.com/eclipse-iofog/iofogctl/internal/config" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" - "github.com/eclipse-iofog/iofogctl/pkg/util" ) type LocalExecutor struct { - controlPlane *rsc.LocalControlPlane - namespace string - name string - localControllerConfig *install.LocalContainerConfig + controlPlane *rsc.LocalControlPlane + namespace string + name string } func NewLocalExecutor(controlPlane *rsc.LocalControlPlane, namespace, name string) *LocalExecutor { - exe := &LocalExecutor{ - controlPlane: controlPlane, - namespace: namespace, - name: name, - localControllerConfig: install.NewLocalControllerConfig("", install.Credentials{}, install.Auth{}, install.Database{}, install.Events{}, nil), + return &LocalExecutor{ + controlPlane: controlPlane, + namespace: namespace, + name: name, } - return exe } func (exe *LocalExecutor) GetName() string { @@ -48,17 +28,6 @@ func (exe *LocalExecutor) Execute() error { if err != nil { return err } - client, err := install.NewLocalContainerClient() - if err != nil { - return err - } - // Get container config - // Clean container - if errClean := client.CleanContainer(exe.localControllerConfig.ContainerName); errClean != nil { - util.PrintNotify(fmt.Sprintf("Could not clean Controller container: %v", errClean)) - } - - // Update config if err := ns.DeleteController(exe.name); err != nil { return err } diff --git a/internal/delete/controller/remote.go b/internal/delete/controller/remote.go index 4569ce01b..e8b88b44b 100644 --- a/internal/delete/controller/remote.go +++ b/internal/delete/controller/remote.go @@ -1,25 +1,10 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletecontroller import ( - // "fmt" - "github.com/eclipse-iofog/iofogctl/internal/config" + deleteremotecontrolplane "github.com/eclipse-iofog/iofogctl/internal/delete/controlplane/remote" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - // "github.com/eclipse-iofog/iofogctl/pkg/iofog" - "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -42,54 +27,26 @@ func (exe *RemoteExecutor) GetName() string { } func (exe *RemoteExecutor) Execute() error { - // Get controller from config baseCtrl, err := exe.controlPlane.GetController(exe.name) if err != nil { return err } - // Assert dynamic type ctrl, ok := baseCtrl.(*rsc.RemoteController) if !ok { return util.NewInternalError("Could not assert Controller type to Remote Controller") } - // Try to remove default router TODO: skipping right now as systemAgent is not deployed with isSystem - // sshAgent, err := install.NewRemoteAgent(ctrl.SSH.User, - // ctrl.Host, - // ctrl.SSH.Port, - // ctrl.SSH.KeyFile, - // iofog.VanillaRemoteAgentName, - // "") - // if err != nil { - // return err - // } - // if err = sshAgent.Uninstall(); err != nil { - // util.PrintNotify(fmt.Sprintf("Failed to stop daemon on Agent %s. %s", iofog.VanillaRemoteAgentName, err.Error())) - // } - - // Instantiate Controller uninstaller - controllerOptions := &install.ControllerOptions{ - User: ctrl.SSH.User, - Host: ctrl.Host, - Port: ctrl.SSH.Port, - PrivKeyFilename: ctrl.SSH.KeyFile, - } - installer, err := install.NewController(controllerOptions) - if err != nil { - return err - } - - // Uninstall Controller - if err := installer.Uninstall(); err != nil { + if err := deleteremotecontrolplane.TeardownRemoteControllerHost(exe.namespace, exe.controlPlane, ctrl); err != nil { return err } - // Update config ns, err := config.GetNamespace(exe.namespace) if err != nil { return err } + _ = ns.DeleteAgent(exe.name) + clientutil.InvalidateAgentCache(exe.namespace) if err := ns.DeleteController(exe.name); err != nil { return err } diff --git a/internal/delete/controller/remote_test.go b/internal/delete/controller/remote_test.go new file mode 100644 index 000000000..5ecdbc52e --- /dev/null +++ b/internal/delete/controller/remote_test.go @@ -0,0 +1,18 @@ +package deletecontroller + +import ( + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRemoteControllerDeleteDoesNotUseLegacyUninstall(t *testing.T) { + src, err := os.ReadFile("remote.go") + require.NoError(t, err) + body := string(src) + require.NotContains(t, body, "NewController(") + require.NotContains(t, body, "install.ControllerOptions") + require.True(t, strings.Contains(body, "TeardownRemoteControllerHost")) +} diff --git a/internal/delete/controlplane/factory.go b/internal/delete/controlplane/factory.go index c95869a7c..4e7882f27 100644 --- a/internal/delete/controlplane/factory.go +++ b/internal/delete/controlplane/factory.go @@ -10,7 +10,7 @@ import ( "github.com/eclipse-iofog/iofogctl/pkg/util" ) -func NewExecutor(namespace string) (execute.Executor, error) { +func NewExecutor(namespace string, deleteNamespace bool) (execute.Executor, error) { ns, err := config.GetNamespace(namespace) if err != nil { return nil, err @@ -22,7 +22,7 @@ func NewExecutor(namespace string) (execute.Executor, error) { switch baseControlPlane.(type) { case *rsc.KubernetesControlPlane: - return deletek8scontrolplane.NewExecutor(namespace) + return deletek8scontrolplane.NewExecutor(namespace, deleteNamespace) case *rsc.RemoteControlPlane: return deleteremotecontrolplane.NewExecutor(namespace) case *rsc.LocalControlPlane: diff --git a/internal/delete/controlplane/k8s/k8s.go b/internal/delete/controlplane/k8s/k8s.go index 85f70426a..eda5101b2 100644 --- a/internal/delete/controlplane/k8s/k8s.go +++ b/internal/delete/controlplane/k8s/k8s.go @@ -1,45 +1,32 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletek8scontrolplane import ( "github.com/eclipse-iofog/iofogctl/internal/config" "github.com/eclipse-iofog/iofogctl/internal/execute" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/internal/trust" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) type Executor struct { - namespace string + namespace string + deleteNamespace bool } -func NewExecutor(namespace string) (execute.Executor, error) { +func NewExecutor(namespace string, deleteNamespace bool) (execute.Executor, error) { exe := &Executor{ - namespace: namespace, + namespace: namespace, + deleteNamespace: deleteNamespace, } return exe, nil } -// GetName returns application name func (exe *Executor) GetName() string { return "Delete Control Plane" } -// Execute deletes application by deleting its associated flow func (exe *Executor) Execute() (err error) { - // Get Control Plane ns, err := config.GetNamespace(exe.namespace) if err != nil { return err @@ -54,20 +41,19 @@ func (exe *Executor) Execute() (err error) { return util.NewError("Could not convert Control Plane to Kubernetes Control Plane") } - // Instantiate Kubernetes object k8s, err := install.NewKubernetes(controlPlane.KubeConfig, exe.namespace) if err != nil { return err } - // Delete Controller on cluster - err = k8s.DeleteControlPlane() - if err != nil { + if err = k8s.DeleteControlPlane(exe.deleteNamespace); err != nil { return err } - // Delete Control Plane in config - ns.DeleteControlPlane() + if err = trust.RemoveCA(exe.namespace); err != nil { + return err + } + ns.DeleteControlPlane() return config.Flush() } diff --git a/internal/delete/controlplane/local/local.go b/internal/delete/controlplane/local/local.go index d9ad445a2..14ba94e54 100644 --- a/internal/delete/controlplane/local/local.go +++ b/internal/delete/controlplane/local/local.go @@ -1,21 +1,7 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletelocalcontrolplane import ( "github.com/eclipse-iofog/iofogctl/internal/config" - deletecontroller "github.com/eclipse-iofog/iofogctl/internal/delete/controller" "github.com/eclipse-iofog/iofogctl/internal/execute" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" "github.com/eclipse-iofog/iofogctl/pkg/util" @@ -37,9 +23,7 @@ func (exe *Executor) GetName() string { return "Delete Control Plane" } -// Execute deletes application by deleting its associated flow func (exe *Executor) Execute() (err error) { - // Get Control Plane ns, err := config.GetNamespace(exe.namespace) if err != nil { return err @@ -54,12 +38,19 @@ func (exe *Executor) Execute() (err error) { return util.NewError("Could not convert Control Plane to Local Control Plane") } - executor := deletecontroller.NewLocalExecutor(controlPlane, exe.namespace, controlPlane.Controller.GetName()) - if err := executor.Execute(); err != nil { - return err + name := ControlPlaneSystemAgentName(controlPlane) + + if controlPlane.SystemAgent != nil { + if err := teardownEdgeletControlPlane(exe.namespace, controlPlane, name); err != nil { + return err + } } - // Delete Control Plane in config ns.DeleteControlPlane() return config.Flush() } + +// IsLocalEdgeletControlPlane reports whether the namespace uses edgelet host teardown. +func IsLocalEdgeletControlPlane(cp rsc.ControlPlane) bool { + return isLocalEdgeletControlPlane(cp) +} diff --git a/internal/delete/controlplane/local/teardown.go b/internal/delete/controlplane/local/teardown.go new file mode 100644 index 000000000..f2568fdaf --- /dev/null +++ b/internal/delete/controlplane/local/teardown.go @@ -0,0 +1,149 @@ +package deletelocalcontrolplane + +import ( + "fmt" + "strings" + + "github.com/eclipse-iofog/iofogctl/internal/config" + deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" + deploylocalcontrolplane "github.com/eclipse-iofog/iofogctl/internal/deploy/controlplane/local" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func teardownEdgeletControlPlane(namespace string, cp *rsc.LocalControlPlane, name string) error { + util.SpinStart("Deleting Control Plane") + + agentUUID := resolveSystemAgentUUID(namespace, name) + edgelet, err := deploylocalcontrolplane.BuildEdgeletForTeardown(cp, namespace, name, agentUUID) + if err != nil { + return err + } + + // Remove the system agent from the controller while it is still reachable. + if agentUUID != "" { + deleteSystemAgentFromController(namespace, name, agentUUID) + } + + install.Verbose("Deprovisioning edgelet on " + name) + if err := edgelet.Deprovision(); err != nil { + util.PrintNotify(fmt.Sprintf("Could not deprovision edgelet: %v", err)) + } + + install.Verbose("Deleting edgelet control plane on " + name) + if err := edgelet.DeleteControlPlane(); err != nil { + util.PrintNotify(fmt.Sprintf("Could not delete edgelet control plane: %v", err)) + } + + clientutil.InvalidateAgentCache(namespace) + + install.Verbose("Uninstalling edgelet from " + name) + if err := edgelet.Uninstall(true); err != nil { + util.PrintNotify(fmt.Sprintf("Could not uninstall edgelet: %v", err)) + } + + if err := cleanLocalEdgeletMicroserviceContainers(cp); err != nil { + util.PrintNotify(fmt.Sprintf("Could not clean microservice containers: %v", err)) + } + + ns, err := config.GetNamespace(namespace) + if err != nil { + return err + } + _ = ns.DeleteAgent(name) + clientutil.InvalidateAgentCache(namespace) + return config.Flush() +} + +func deleteSystemAgentFromController(namespace, name, agentUUID string) { + ctrl, err := clientutil.NewControllerClient(namespace) + if err != nil { + util.PrintNotify(fmt.Sprintf("Could not delete Agent %s from the Controller: %v", name, err)) + return + } + if err := ctrl.DeleteAgent(agentUUID); err != nil && !isIgnorableControllerAgentDeleteError(err) { + util.PrintNotify(fmt.Sprintf("Could not delete Agent %s from the Controller: %v", name, err)) + } +} + +func resolveSystemAgentUUID(namespace, name string) string { + ns, err := config.GetNamespace(namespace) + if err != nil { + return "" + } + if agent, err := ns.GetAgent(name); err == nil && agent.GetUUID() != "" { + return agent.GetUUID() + } + backendAgents, err := clientutil.GetBackendAgents(namespace) + if err != nil { + return "" + } + for idx := range backendAgents { + if backendAgents[idx].Name == name { + return backendAgents[idx].UUID + } + } + return "" +} + +func cleanLocalEdgeletMicroserviceContainers(cp *rsc.LocalControlPlane) error { + sys := cp.SystemAgent + var cfg *rsc.AgentConfiguration + var pkg rsc.Package + if sys != nil { + cfg = sys.AgentConfiguration + pkg = sys.Package + } + cfg = deployairgap.EnsureAgentConfig(cfg) + installCfg := deployairgap.EdgeletInstallConfig(deployairgap.LocalEdgeletHostOS(), cfg, pkg) + client, err := install.NewLocalContainerClientFromEdgeletCfg(installCfg) + if err != nil { + return err + } + containers, err := client.ListContainers() + if err != nil { + return err + } + for idx := range containers { + container := &containers[idx] + for _, containerName := range container.Names { + if strings.HasPrefix(containerName, "/iofog_") { + if errClean := client.CleanContainerByID(container.ID); errClean != nil { + util.PrintNotify(fmt.Sprintf("Could not clean Microservice container: %v", errClean)) + } + } + } + } + return nil +} + +func isIgnorableControllerAgentDeleteError(err error) bool { + if err == nil { + return false + } + if util.IsNotFoundError(err) { + return true + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "not found") || + strings.Contains(msg, "notfound") || + strings.Contains(msg, "connection refused") || + strings.Contains(msg, "connect: connection refused") || + strings.Contains(msg, "network is unreachable") || + strings.Contains(msg, "no route to host") +} + +func isLocalEdgeletControlPlane(cp rsc.ControlPlane) bool { + localCP, ok := cp.(*rsc.LocalControlPlane) + return ok && localCP.SystemAgent != nil +} + +// ControlPlaneSystemAgentName returns the agent name tied to a local edgelet control plane. +func ControlPlaneSystemAgentName(cp *rsc.LocalControlPlane) string { + if controllers := cp.GetControllers(); len(controllers) > 0 { + return controllers[0].GetName() + } + return "local" +} diff --git a/internal/delete/controlplane/remote/remote.go b/internal/delete/controlplane/remote/remote.go index f57255f46..736eef4c3 100644 --- a/internal/delete/controlplane/remote/remote.go +++ b/internal/delete/controlplane/remote/remote.go @@ -1,23 +1,10 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteremotecontrolplane import ( "github.com/eclipse-iofog/iofogctl/internal/config" - deletecontroller "github.com/eclipse-iofog/iofogctl/internal/delete/controller" "github.com/eclipse-iofog/iofogctl/internal/execute" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/internal/trust" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -25,21 +12,21 @@ type Executor struct { namespace string } +type hostTeardownExecutor struct { + namespace string + cp *rsc.RemoteControlPlane + ctrl *rsc.RemoteController +} + func NewExecutor(namespace string) (execute.Executor, error) { - exe := &Executor{ - namespace: namespace, - } - return exe, nil + return &Executor{namespace: namespace}, nil } -// GetName returns application name func (exe *Executor) GetName() string { return "Delete Control Plane" } -// Execute deletes application by deleting its associated flow func (exe *Executor) Execute() (err error) { - // Get Control Plane ns, err := config.GetNamespace(exe.namespace) if err != nil { return err @@ -50,26 +37,51 @@ func (exe *Executor) Execute() (err error) { } controlPlane, ok := baseControlPlane.(*rsc.RemoteControlPlane) if !ok { - return util.NewError("Could not Convert Controller to Remote Controller") + return util.NewError("Could not convert Control Plane to Remote Control Plane") } controllers := controlPlane.GetControllers() executors := make([]execute.Executor, len(controllers)) for idx := range controllers { - controller := controllers[idx] - exe := deletecontroller.NewRemoteExecutor(controlPlane, exe.namespace, controller.GetName()) - executors[idx] = exe + controller, ok := controllers[idx].(*rsc.RemoteController) + if !ok { + return util.NewInternalError("Could not convert Controller to Remote Controller") + } + executors[idx] = newHostTeardownExecutor(exe.namespace, controlPlane, controller) } if err := runExecutors(executors); err != nil { return err } - // Delete Control Plane in config + for idx := range controllers { + _ = ns.DeleteAgent(controllers[idx].GetName()) + } + + if err := trust.RemoveCA(exe.namespace); err != nil { + return err + } + ns.DeleteControlPlane() return config.Flush() } +func newHostTeardownExecutor(namespace string, cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController) *hostTeardownExecutor { + return &hostTeardownExecutor{ + namespace: namespace, + cp: cp, + ctrl: ctrl, + } +} + +func (exe *hostTeardownExecutor) GetName() string { + return exe.ctrl.Name +} + +func (exe *hostTeardownExecutor) Execute() error { + return TeardownRemoteControllerHost(exe.namespace, exe.cp, exe.ctrl) +} + func runExecutors(executors []execute.Executor) error { if errs, _ := execute.ForParallel(executors); len(errs) > 0 { return execute.CoalesceErrors(errs) diff --git a/internal/delete/controlplane/remote/teardown.go b/internal/delete/controlplane/remote/teardown.go new file mode 100644 index 000000000..1e0c728c2 --- /dev/null +++ b/internal/delete/controlplane/remote/teardown.go @@ -0,0 +1,108 @@ +package deleteremotecontrolplane + +import ( + "fmt" + "strings" + + "github.com/eclipse-iofog/iofogctl/internal/config" + deployremotecontrolplane "github.com/eclipse-iofog/iofogctl/internal/deploy/controlplane/remote" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type edgeletHostTeardown interface { + Deprovision() error + DeleteControlPlane() error + Uninstall(removeData bool) error +} + +var ( + buildRemoteEdgeletFn = deployremotecontrolplane.BuildRemoteEdgelet + deleteSystemAgentFn = deleteSystemAgentFromController + edgeletDeprovisionFn = func(e edgeletHostTeardown) error { return e.Deprovision() } + edgeletDeleteCPFn = func(e edgeletHostTeardown) error { return e.DeleteControlPlane() } + edgeletUninstallFn = func(e edgeletHostTeardown) error { return e.Uninstall(true) } +) + +// TeardownRemoteControllerHost removes edgelet and control plane workloads from one remote host. +func TeardownRemoteControllerHost(namespace string, cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController) error { + util.SpinStart("Deleting Control Plane on " + ctrl.Name) + + agentUUID := resolveSystemAgentUUID(namespace, ctrl.Name) + edgelet, err := buildRemoteEdgeletFn(cp, ctrl, agentUUID) + if err != nil { + return err + } + + if agentUUID != "" { + deleteSystemAgentFn(namespace, ctrl.Name, agentUUID) + } + + install.Verbose("Deprovisioning edgelet on " + ctrl.Name) + if err := edgeletDeprovisionFn(edgelet); err != nil { + util.PrintNotify(fmt.Sprintf("Could not deprovision edgelet: %v", err)) + } + + install.Verbose("Deleting edgelet control plane on " + ctrl.Name) + if err := edgeletDeleteCPFn(edgelet); err != nil { + util.PrintNotify(fmt.Sprintf("Could not delete edgelet control plane: %v", err)) + } + + clientutil.InvalidateAgentCache(namespace) + + install.Verbose("Uninstalling edgelet from " + ctrl.Name) + if err := edgeletUninstallFn(edgelet); err != nil { + util.PrintNotify(fmt.Sprintf("Could not uninstall edgelet: %v", err)) + } + + return nil +} + +func deleteSystemAgentFromController(namespace, name, agentUUID string) { + ctrl, err := clientutil.NewControllerClient(namespace) + if err != nil { + util.PrintNotify(fmt.Sprintf("Could not delete Agent %s from the Controller: %v", name, err)) + return + } + if err := ctrl.DeleteAgent(agentUUID); err != nil && !isIgnorableControllerAgentDeleteError(err) { + util.PrintNotify(fmt.Sprintf("Could not delete Agent %s from the Controller: %v", name, err)) + } +} + +func resolveSystemAgentUUID(namespace, name string) string { + ns, err := config.GetNamespace(namespace) + if err != nil { + return "" + } + if agent, err := ns.GetAgent(name); err == nil && agent.GetUUID() != "" { + return agent.GetUUID() + } + backendAgents, err := clientutil.GetBackendAgents(namespace) + if err != nil { + return "" + } + for idx := range backendAgents { + if backendAgents[idx].Name == name { + return backendAgents[idx].UUID + } + } + return "" +} + +func isIgnorableControllerAgentDeleteError(err error) bool { + if err == nil { + return false + } + if util.IsNotFoundError(err) { + return true + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "not found") || + strings.Contains(msg, "notfound") || + strings.Contains(msg, "connection refused") || + strings.Contains(msg, "connect: connection refused") || + strings.Contains(msg, "network is unreachable") || + strings.Contains(msg, "no route to host") +} diff --git a/internal/delete/controlplane/remote/teardown_test.go b/internal/delete/controlplane/remote/teardown_test.go new file mode 100644 index 000000000..47f3d1f78 --- /dev/null +++ b/internal/delete/controlplane/remote/teardown_test.go @@ -0,0 +1,140 @@ +package deleteremotecontrolplane + +import ( + "errors" + "testing" + + "github.com/eclipse-iofog/iofogctl/internal/config" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/internal/trust" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/stretchr/testify/require" +) + +type stubEdgeletHost struct { + deprovisionErr error + deleteCPErr error + uninstallErr error + deprovisionCalls int + deleteCPCalls int + uninstallCalls int +} + +func (s *stubEdgeletHost) Deprovision() error { + s.deprovisionCalls++ + return s.deprovisionErr +} + +func (s *stubEdgeletHost) DeleteControlPlane() error { + s.deleteCPCalls++ + return s.deleteCPErr +} + +func (s *stubEdgeletHost) Uninstall(bool) error { + s.uninstallCalls++ + return s.uninstallErr +} + +func TestIsIgnorableControllerAgentDeleteError(t *testing.T) { + require.True(t, isIgnorableControllerAgentDeleteError(util.NewNotFoundError("agent"))) + require.True(t, isIgnorableControllerAgentDeleteError(errors.New("connection refused"))) + require.True(t, isIgnorableControllerAgentDeleteError(errors.New("connect: connection refused"))) + require.False(t, isIgnorableControllerAgentDeleteError(errors.New("permission denied"))) +} + +func TestResolveSystemAgentUUIDFromConfig(t *testing.T) { + dir := t.TempDir() + config.Init(dir) + + ns, err := config.GetNamespace("default") + require.NoError(t, err) + require.NoError(t, ns.AddAgent(&rsc.RemoteAgent{ + Name: "remote-1", + UUID: "uuid-from-config", + })) + require.NoError(t, config.Flush()) + + require.Equal(t, "uuid-from-config", resolveSystemAgentUUID("default", "remote-1")) +} + +func TestTeardownRemoteControllerHostContinuesOnEdgeletErrors(t *testing.T) { + t.Cleanup(resetTeardownHooks()) + + stub := &stubEdgeletHost{ + deprovisionErr: errors.New("deprovision failed"), + deleteCPErr: errors.New("delete cp failed"), + uninstallErr: errors.New("uninstall failed"), + } + buildRemoteEdgeletFn = func(*rsc.RemoteControlPlane, *rsc.RemoteController, string) (*install.RemoteEdgelet, error) { + return &install.RemoteEdgelet{}, nil + } + edgeletDeprovisionFn = func(edgeletHostTeardown) error { return stub.Deprovision() } + edgeletDeleteCPFn = func(edgeletHostTeardown) error { return stub.DeleteControlPlane() } + edgeletUninstallFn = func(edgeletHostTeardown) error { return stub.Uninstall(true) } + deleteSystemAgentFn = func(string, string, string) {} + + cp := &rsc.RemoteControlPlane{} + ctrl := &rsc.RemoteController{Name: "remote-1", Host: "10.0.0.1"} + + require.NoError(t, TeardownRemoteControllerHost("default", cp, ctrl)) + require.Equal(t, 1, stub.deprovisionCalls) + require.Equal(t, 1, stub.deleteCPCalls) + require.Equal(t, 1, stub.uninstallCalls) +} + +func TestExecutorExecuteRemovesCA(t *testing.T) { + t.Cleanup(resetTeardownHooks()) + + dir := t.TempDir() + config.Init(dir) + + arch := "amd64" + cp := &rsc.RemoteControlPlane{ + Controllers: []rsc.RemoteController{{ + Name: "remote-1", + Host: "10.0.0.1", + SystemAgent: &rsc.SystemAgentConfig{ + AgentConfiguration: &rsc.AgentConfiguration{Arch: &arch}, + }, + }}, + } + ns, err := config.GetNamespace("default") + require.NoError(t, err) + ns.SetControlPlane(cp) + require.NoError(t, config.Flush()) + require.NoError(t, trust.StoreCA("default", "dGVzdA==")) + + buildRemoteEdgeletFn = func(*rsc.RemoteControlPlane, *rsc.RemoteController, string) (*install.RemoteEdgelet, error) { + return &install.RemoteEdgelet{}, nil + } + edgeletDeprovisionFn = func(edgeletHostTeardown) error { return nil } + edgeletDeleteCPFn = func(edgeletHostTeardown) error { return nil } + edgeletUninstallFn = func(edgeletHostTeardown) error { return nil } + deleteSystemAgentFn = func(string, string, string) {} + + exe, err := NewExecutor("default") + require.NoError(t, err) + require.NoError(t, exe.Execute()) + require.False(t, trust.HasCA("default")) + + ns, err = config.GetNamespace("default") + require.NoError(t, err) + _, err = ns.GetControlPlane() + require.Error(t, err) +} + +func resetTeardownHooks() func() { + prevBuild := buildRemoteEdgeletFn + prevDeleteAgent := deleteSystemAgentFn + prevDeprovision := edgeletDeprovisionFn + prevDeleteCP := edgeletDeleteCPFn + prevUninstall := edgeletUninstallFn + return func() { + buildRemoteEdgeletFn = prevBuild + deleteSystemAgentFn = prevDeleteAgent + edgeletDeprovisionFn = prevDeprovision + edgeletDeleteCPFn = prevDeleteCP + edgeletUninstallFn = prevUninstall + } +} diff --git a/internal/delete/edgeresource/factory.go b/internal/delete/edgeresource/factory.go deleted file mode 100644 index 11fe694b5..000000000 --- a/internal/delete/edgeresource/factory.go +++ /dev/null @@ -1,57 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package deleteedgeresource - -import ( - "fmt" - - "github.com/eclipse-iofog/iofogctl/internal/config" - "github.com/eclipse-iofog/iofogctl/internal/execute" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" -) - -type executor struct { - namespace string - name string - version string -} - -func (exe executor) GetName() string { - return fmt.Sprintf("%s/%s", exe.name, exe.version) -} - -func (exe executor) Execute() (err error) { - if _, err = config.GetNamespace(exe.namespace); err != nil { - return - } - - // Connect to Controller - clt, err := clientutil.NewControllerClient(exe.namespace) - if err != nil { - return - } - - if err = clt.DeleteEdgeResource(exe.name, exe.version); err != nil { - return - } - return -} - -func NewExecutor(namespace, name, version string) (exe execute.Executor) { - return executor{ - namespace: namespace, - name: name, - version: version, - } -} diff --git a/internal/delete/execute.go b/internal/delete/execute.go index a406be410..1f4e4c982 100644 --- a/internal/delete/execute.go +++ b/internal/delete/execute.go @@ -1,19 +1,7 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package delete import ( + "errors" "fmt" "github.com/eclipse-iofog/iofogctl/internal/config" @@ -42,9 +30,10 @@ import ( ) type Options struct { - Namespace string - InputFile string - Soft bool + Namespace string + InputFile string + Soft bool + DeleteNamespace bool } var kindOrder = []config.Kind{ @@ -81,7 +70,7 @@ var kindHandlers = map[config.Kind]func(*execute.KindHandlerOpt) (execute.Execut return deletemicroservice.NewExecutor(opt.Namespace, opt.Name) }, config.KubernetesControlPlaneKind: func(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { - return deletek8scontrolplane.NewExecutor(opt.Namespace) + return deletek8scontrolplane.NewExecutor(opt.Namespace, opt.DeleteNamespace) }, config.RemoteControlPlaneKind: func(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { return deleteremotecontrolplane.NewExecutor(opt.Namespace) @@ -146,7 +135,7 @@ var kindHandlers = map[config.Kind]func(*execute.KindHandlerOpt) (execute.Execut } func Execute(opt *Options) error { - executorsMap, err := execute.GetExecutorsFromYAML(opt.InputFile, opt.Namespace, kindHandlers) + executorsMap, err := execute.GetExecutorsFromYAML(opt.InputFile, opt.Namespace, kindHandlers, opt.DeleteNamespace) if err != nil { return err } @@ -155,7 +144,8 @@ func Execute(opt *Options) error { for idx := range kindOrder { if errs := execute.RunExecutors(executorsMap[kindOrder[idx]], fmt.Sprintf("delete %s", kindOrder[idx])); len(errs) > 0 { for _, err := range errs { - if _, ok := err.(*util.NotFoundError); !ok { + notFoundError := &util.NotFoundError{} + if errors.As(err, ¬FoundError) { return execute.CoalesceErrors(errs) } util.PrintNotify(fmt.Sprintf("Warning: %s %s.", kindOrder[idx], err.Error())) diff --git a/internal/delete/microservice/microservice.go b/internal/delete/microservice/microservice.go index a001cf228..1d9d075e7 100644 --- a/internal/delete/microservice/microservice.go +++ b/internal/delete/microservice/microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletecatalogitem import ( @@ -38,7 +25,7 @@ func (exe *Executor) GetName() string { return exe.name } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() (err error) { util.SpinStart("Deleting Microservice") // Init remote resources diff --git a/internal/delete/namespace/namespace.go b/internal/delete/namespace/namespace.go index 3cdd34980..666c144e3 100644 --- a/internal/delete/namespace/namespace.go +++ b/internal/delete/namespace/namespace.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletemicroservice import ( @@ -42,7 +29,7 @@ func Execute(name string, force bool) error { // Handle delete all if force && (hasAgents || hasControllers) { - if err := delete.Execute(name, false, force); err != nil { + if err := delete.Execute(name, false, false, force); err != nil { return err } } diff --git a/internal/delete/registry/registry.go b/internal/delete/registry/registry.go index 2eafdc5f5..021f7f71e 100644 --- a/internal/delete/registry/registry.go +++ b/internal/delete/registry/registry.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteregistry import ( @@ -44,7 +31,7 @@ func (exe *Executor) GetName() string { return strconv.Itoa(exe.id) } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() error { util.SpinStart("Deleting Registry") // Init remote resources diff --git a/internal/delete/role/role.go b/internal/delete/role/role.go index 7ae87bf84..9f371c8a2 100644 --- a/internal/delete/role/role.go +++ b/internal/delete/role/role.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleterole import ( diff --git a/internal/delete/rolebinding/rolebinding.go b/internal/delete/rolebinding/rolebinding.go index d0f44369d..5110e9fd4 100644 --- a/internal/delete/rolebinding/rolebinding.go +++ b/internal/delete/rolebinding/rolebinding.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleterolebinding import ( diff --git a/internal/delete/secret/secret.go b/internal/delete/secret/secret.go index 30faac866..67808037b 100644 --- a/internal/delete/secret/secret.go +++ b/internal/delete/secret/secret.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletesecret import ( @@ -38,7 +25,7 @@ func (exe *Executor) GetName() string { return exe.name } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() error { util.SpinStart("Deleting Secret") // Init remote resources diff --git a/internal/delete/service/service.go b/internal/delete/service/service.go index 4a1939baa..37ad2f64a 100644 --- a/internal/delete/service/service.go +++ b/internal/delete/service/service.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteservice import ( @@ -38,7 +25,7 @@ func (exe *Executor) GetName() string { return exe.name } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() error { util.SpinStart("Deleting Service") // Init remote resources diff --git a/internal/delete/serviceaccount/serviceaccount.go b/internal/delete/serviceaccount/serviceaccount.go index b96998102..487a4bafe 100644 --- a/internal/delete/serviceaccount/serviceaccount.go +++ b/internal/delete/serviceaccount/serviceaccount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteserviceaccount import ( diff --git a/internal/delete/template/execute.go b/internal/delete/template/execute.go index fe9a7c659..dfa831ce5 100644 --- a/internal/delete/template/execute.go +++ b/internal/delete/template/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deleteapplicationtemplate import ( @@ -52,7 +39,7 @@ func (exe *Executor) GetName() string { return exe.name } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() error { util.SpinStart("Deleting Application Template") clt, err := clientutil.NewControllerClient(exe.namespace) diff --git a/internal/delete/volume/local.go b/internal/delete/volume/local.go index 97f7228f1..59344d995 100644 --- a/internal/delete/volume/local.go +++ b/internal/delete/volume/local.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletevolume import ( diff --git a/internal/delete/volume/remote.go b/internal/delete/volume/remote.go index a89687226..a3df84656 100644 --- a/internal/delete/volume/remote.go +++ b/internal/delete/volume/remote.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletevolume import ( @@ -32,6 +19,7 @@ func deleteRemote(agent *rsc.RemoteAgent, volume *rsc.Volume) error { if err != nil { return err } + ssh.SetPort(agent.SSH.Port) if err := ssh.Connect(); err != nil { return err } diff --git a/internal/delete/volume/volume.go b/internal/delete/volume/volume.go index 5ef54237c..035e622af 100644 --- a/internal/delete/volume/volume.go +++ b/internal/delete/volume/volume.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletevolume import ( @@ -43,7 +30,7 @@ func (exe *Executor) GetName() string { return "Delete Volume " + exe.volumeName } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() error { util.SpinStart("Deleting Volume") volume, err := exe.ns.GetVolume(exe.volumeName) diff --git a/internal/delete/volumemount/volume_mount.go b/internal/delete/volumemount/volume_mount.go index 9ded5ee39..de67bca7d 100644 --- a/internal/delete/volumemount/volume_mount.go +++ b/internal/delete/volumemount/volume_mount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deletevolumemount import ( @@ -38,7 +25,7 @@ func (exe *Executor) GetName() string { return exe.name } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() error { util.SpinStart("Deleting Volume Mount") // Init remote resources diff --git a/internal/deploy/agent/edgelet.go b/internal/deploy/agent/edgelet.go new file mode 100644 index 000000000..a3c9700ae --- /dev/null +++ b/internal/deploy/agent/edgelet.go @@ -0,0 +1,65 @@ +package deployagent + +import ( + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + deployvalidate "github.com/eclipse-iofog/iofogctl/internal/deploy/validate" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type edgeletAgent interface { + Bootstrap() error + Configure(controllerEndpoint string, user install.IofogUser, sdkOpt client.Options) (string, error) + SetVersion(version string) error + SetContainerImage(image string) error + SetAirgap(binPath string) error + CustomizeProcedures(dir string, procs *install.EdgeletProcedures) error +} + +func ensureLocalAgentHost(agent *rsc.LocalAgent) error { + cfg := agent.Config + if cfg == nil { + cfg = &rsc.AgentConfiguration{} + agent.Config = cfg + } + if cfg.Host != nil && strings.TrimSpace(*cfg.Host) != "" { + return nil + } + if strings.TrimSpace(agent.Host) != "" { + host := strings.TrimSpace(agent.Host) + cfg.Host = &host + return nil + } + ip, err := util.DetectLocalHostIPv4() + if err != nil { + return util.NewInputError("LocalAgent requires spec.config.host or a detectable local IPv4 address") + } + cfg.Host = &ip + agent.Host = ip + return nil +} + +func checkLocalAgentPortAvailable(isSystem bool) error { + return deployvalidate.LocalAgentPortAvailable(isSystem) +} + +func applyEdgeletPackage(agent edgeletAgent, pkg rsc.Package) error { + if pkg.Container.Image != "" { + return agent.SetContainerImage(pkg.Container.Image) + } + if pkg.Version != "" { + return agent.SetVersion(pkg.Version) + } + return nil +} + +func customizeEdgeletProcedures(agent edgeletAgent, scripts *rsc.AgentScripts) error { + if scripts == nil { + return nil + } + procs := install.EdgeletProcedures{AgentProcedures: scripts.AgentProcedures} + return agent.CustomizeProcedures(scripts.Directory, &procs) +} diff --git a/internal/deploy/agent/execute.go b/internal/deploy/agent/execute.go index 3aa793573..cd80e706b 100644 --- a/internal/deploy/agent/execute.go +++ b/internal/deploy/agent/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployagent import ( diff --git a/internal/deploy/agent/factory.go b/internal/deploy/agent/factory.go index 90caed58e..199ca99b2 100644 --- a/internal/deploy/agent/factory.go +++ b/internal/deploy/agent/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployagent import ( @@ -74,13 +61,7 @@ func (facade *facadeExecutor) Execute() (err error) { if err = facade.exe.Execute(); err != nil { return } - // Update: Include system agents in namespace file - // System agents should be saved to namespace file for consistency and management - if err = ns.UpdateAgent(facade.agent); err != nil { - return - } - // Set Agent configuration if provided if agentConfig := facade.agent.GetConfig(); agentConfig != nil { configExe := agentconfig.NewRemoteExecutor(facade.agent.GetName(), agentConfig, facade.namespace, facade.tags) if err := configExe.Execute(); err != nil { @@ -88,6 +69,17 @@ func (facade *facadeExecutor) Execute() (err error) { } } + uuid, err := facade.ProvisionAgent() + if err != nil { + return err + } + facade.agent.SetUUID(uuid) + facade.agent.SetCreatedTime(util.NowUTC()) + + if err = ns.UpdateAgent(facade.agent); err != nil { + return + } + return config.Flush() } diff --git a/internal/deploy/agent/local.go b/internal/deploy/agent/local.go index 6b9332f8f..0658e92a0 100644 --- a/internal/deploy/agent/local.go +++ b/internal/deploy/agent/local.go @@ -1,70 +1,58 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployagent import ( - "fmt" - "regexp" + "context" "github.com/eclipse-iofog/iofogctl/internal/config" + deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) type localExecutor struct { - isSystem bool - namespace string - agent *rsc.LocalAgent - client *install.LocalContainer - localAgentConfig *install.LocalAgentConfig + isSystem bool + namespace string + agent *rsc.LocalAgent + edgelet edgeletAgent } func newLocalExecutor(namespace string, agent *rsc.LocalAgent, isSystem bool) (*localExecutor, error) { - client, err := install.NewLocalContainerClient() - if err != nil { + if err := checkLocalAgentPortAvailable(isSystem); err != nil { return nil, err } - if agent.Config == nil { - agent.Config = &rsc.AgentConfiguration{} + if err := ensureLocalAgentHost(agent); err != nil { + return nil, err } - // Get Controller LocalContainerConfig - controllerContainerConfig := install.NewLocalControllerConfig("", install.Credentials{}, install.Auth{}, install.Database{}, install.Events{}, nil) + agent.Config = deployairgap.EnsureAgentConfig(agent.Config) return &localExecutor{ isSystem: isSystem, namespace: namespace, agent: agent, - client: client, - localAgentConfig: install.NewLocalAgentConfig( - agent.Name, - agent.Container.Image, - controllerContainerConfig, - install.Credentials{ - User: agent.Container.Credentials.User, - Password: agent.Container.Credentials.Password, - }, - isSystem, - agent.Config.TimeZone, - ), }, nil } +func (exe *localExecutor) getEdgelet() (edgeletAgent, error) { + if exe.edgelet != nil { + return exe.edgelet, nil + } + + cfg := deployairgap.EdgeletInstallConfig(deployairgap.LocalEdgeletHostOS(), exe.agent.Config, exe.agent.Package) + edgelet, err := install.NewLocalEdgelet(exe.agent.Name, exe.agent.UUID, cfg) + if err != nil { + return nil, err + } + exe.edgelet = edgelet + return edgelet, nil +} + func (exe *localExecutor) ProvisionAgent() (string, error) { - // Get agent - agent := install.NewLocalAgent(exe.localAgentConfig, exe.client) + edgelet, err := exe.getEdgelet() + if err != nil { + return "", err + } - // Get user ns, err := config.GetNamespace(exe.namespace) if err != nil { return "", err @@ -73,7 +61,7 @@ func (exe *localExecutor) ProvisionAgent() (string, error) { if err != nil { return "", err } - // Try Agent-specific endpoint first + controllerEndpoint := exe.agent.GetControllerEndpoint() if controllerEndpoint == "" { controllerEndpoint, err = controlPlane.GetEndpoint() @@ -82,10 +70,13 @@ func (exe *localExecutor) ProvisionAgent() (string, error) { } } - // Configure the agent with Controller details user := install.IofogUser(controlPlane.GetUser()) user.Password = controlPlane.GetUser().GetRawPassword() - return agent.Configure(controllerEndpoint, user) + opt, err := clientutil.ControllerClientOptions(context.Background(), exe.namespace, controllerEndpoint) + if err != nil { + return "", err + } + return edgelet.Configure(controllerEndpoint, user, opt) } func (exe *localExecutor) GetName() string { @@ -93,51 +84,26 @@ func (exe *localExecutor) GetName() string { } func (exe *localExecutor) Execute() error { - // Deploy agent image - util.SpinStart("Deploying Agent container") - if exe.agent.Container.Image == "" { - exe.agent.Container.Image = exe.localAgentConfig.DefaultImage - } + exe.agent.Config = deployairgap.EnsureAgentConfig(exe.agent.Config) + deployairgap.ResolveAgentDeployment(exe.agent.Config, exe.agent.Package.Container.Image) - // If container already exists, clean it - agentContainerName := exe.localAgentConfig.ContainerName - if _, err := exe.client.GetContainerByName(agentContainerName); err == nil { - if err := exe.client.CleanContainer(agentContainerName); err != nil { - return err - } + edgelet, err := exe.getEdgelet() + if err != nil { + return err } - if _, err := exe.client.DeployContainer(&exe.localAgentConfig.LocalContainerConfig); err != nil { + if err := customizeEdgeletProcedures(edgelet, exe.agent.Scripts); err != nil { return err } - - // Wait for agent - util.SpinStart("Waiting for Agent") - if err := exe.client.WaitForCommand( - install.GetLocalContainerName("agent", exe.isSystem), - regexp.MustCompile("ioFog daemon[ |\t]*: RUNNING"), - "iofog-agent", - "status", - ); err != nil { - if cleanErr := exe.client.CleanContainer(agentContainerName); cleanErr != nil { - util.PrintNotify(fmt.Sprintf("Could not clean container: %v", agentContainerName)) - } + if err := applyEdgeletPackage(edgelet, exe.agent.Package); err != nil { return err } - // Provision agent - util.SpinStart("Provisioning Agent") - uuid, err := exe.ProvisionAgent() - if err != nil { - if cleanErr := exe.client.CleanContainer(agentContainerName); cleanErr != nil { - util.PrintNotify(fmt.Sprintf("Could not clean container: %v", agentContainerName)) - } + util.SpinStart("Installing edgelet") + if err := edgelet.Bootstrap(); err != nil { return err } - // Return new Agent config because variable is a pointer - exe.agent.Host = fmt.Sprintf("%s:%s", exe.localAgentConfig.Host, exe.localAgentConfig.Ports[0].Host) - exe.agent.UUID = uuid - + exe.agent.Host = exe.agent.GetHost() return nil } diff --git a/internal/deploy/agent/remote.go b/internal/deploy/agent/remote.go index bb6049356..37a9b38e2 100644 --- a/internal/deploy/agent/remote.go +++ b/internal/deploy/agent/remote.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployagent import ( @@ -20,9 +7,7 @@ import ( "github.com/eclipse-iofog/iofogctl/internal/config" deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - - // clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - iutil "github.com/eclipse-iofog/iofogctl/internal/util" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" "github.com/eclipse-iofog/iofogctl/pkg/iofog" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" @@ -31,58 +16,46 @@ import ( type remoteExecutor struct { namespace string agent *rsc.RemoteAgent + edgelet edgeletAgent } func newRemoteExecutor(namespace string, agent *rsc.RemoteAgent) *remoteExecutor { - exe := &remoteExecutor{} - exe.namespace = namespace - exe.agent = agent - - return exe + return &remoteExecutor{ + namespace: namespace, + agent: agent, + } } func (exe *remoteExecutor) GetName() string { return exe.agent.Name } -func (exe *remoteExecutor) ProvisionAgent() (string, error) { - var agent *install.RemoteAgent - var err error - // If DeploymentType is nil, default to "container" - // Use NewRemoteContainerAgent if DeploymentType is nil or "container" - // Check if Config is nil and initialize if needed - if exe.agent.Config == nil { - exe.agent.Config = &rsc.AgentConfiguration{} +func (exe *remoteExecutor) getEdgelet() (edgeletAgent, error) { + if exe.edgelet != nil { + return exe.edgelet, nil } - // Check DeploymentType (Config is guaranteed to be non-nil now) - if exe.agent.Config.DeploymentType == nil || *exe.agent.Config.DeploymentType == "container" { - // Use NewRemoteContainerAgent - agent, err = install.NewRemoteContainerAgent( - exe.agent.SSH.User, - exe.agent.Host, - exe.agent.SSH.Port, - exe.agent.SSH.KeyFile, - exe.agent.Name, - exe.agent.UUID, - exe.agent.Config.TimeZone, - ) - if err == nil { - // Set airgap flag if enabled - agent.SetAirgap(exe.agent.Airgap) - } - } else { - // Use NewRemoteAgent for "native" deployment type - agent, err = install.NewRemoteAgent( - exe.agent.SSH.User, - exe.agent.Host, - exe.agent.SSH.Port, - exe.agent.SSH.KeyFile, - exe.agent.Name, - exe.agent.UUID, - ) + exe.agent.Config = deployairgap.EnsureAgentConfig(exe.agent.Config) + cfg := deployairgap.EdgeletInstallConfig("linux", exe.agent.Config, exe.agent.Package) + + edgelet, err := install.NewRemoteEdgelet( + exe.agent.SSH.User, + exe.agent.Host, + exe.agent.SSH.Port, + exe.agent.SSH.KeyFile, + exe.agent.Name, + exe.agent.UUID, + cfg, + ) + if err != nil { + return nil, err } + exe.edgelet = edgelet + return edgelet, nil +} +func (exe *remoteExecutor) ProvisionAgent() (string, error) { + edgelet, err := exe.getEdgelet() if err != nil { return "", err } @@ -95,7 +68,7 @@ func (exe *remoteExecutor) ProvisionAgent() (string, error) { if err != nil { return "", err } - // Try Agent-specific endpoint first + controllerEndpoint := exe.agent.GetControllerEndpoint() if controllerEndpoint == "" { controllerEndpoint, err = controlPlane.GetEndpoint() @@ -104,43 +77,16 @@ func (exe *remoteExecutor) ProvisionAgent() (string, error) { } } - // Configure the agent with Controller details user := install.IofogUser(controlPlane.GetUser()) user.Password = controlPlane.GetUser().GetRawPassword() - // Get Config before provision and set iofog-agent config - agentConfig := exe.agent.GetConfig() - - // Check if agentConfig is empty - if agentConfig == nil { - util.PrintNotify(fmt.Sprintf("Skipping initial agent configuration for %s as agent config parameters are empty. Default config parameters will be used.", exe.agent.Name)) - - } else { - var fogType *string - if agentConfig.FogType == nil { - auto := "auto" - fogType = &auto - } else { - fogType = agentConfig.FogType - } - err = agent.SetInitialConfig( - agentConfig.Name, - // agentConfig.Location, - // agentConfig.Latitude, - // agentConfig.Longitude, - // agentConfig.Description, - *fogType, - agentConfig.AgentConfiguration, // Pass the embedded client.AgentConfiguration - ) - if err != nil { - return "", err - } + opt, err := clientutil.ControllerClientOptions(context.Background(), exe.namespace, controllerEndpoint) + if err != nil { + return "", err } - return agent.Configure(controllerEndpoint, user) + return edgelet.Configure(controllerEndpoint, user, opt) } -// Deploy iofog-agent stack on an agent host func (exe *remoteExecutor) Execute() (err error) { - // Get Control Plane ns, err := config.GetNamespace(exe.namespace) if err != nil { return err @@ -148,156 +94,74 @@ func (exe *remoteExecutor) Execute() (err error) { controlPlane, err := ns.GetControlPlane() if err != nil || len(controlPlane.GetControllers()) == 0 { util.PrintError("You must deploy a Controller to a namespace before deploying any Agents") - return - } - - var agent *install.RemoteAgent - // If DeploymentType is nil, default to "container" - // Use NewRemoteContainerAgent if DeploymentType is nil, "container", or if container image is specified - // Check if Config is nil and initialize if needed - if exe.agent.Config == nil { - exe.agent.Config = &rsc.AgentConfiguration{} + return err } - useContainer := false - // Check DeploymentType (Config is guaranteed to be non-nil now) - if exe.agent.Config.DeploymentType == nil { - useContainer = true - } else if *exe.agent.Config.DeploymentType == "container" { - useContainer = true - } - // Check if container image is specified (Package is a value type, always initialized) - if !useContainer && exe.agent.Package.Container.Image != "" { - useContainer = true - } + exe.agent.Config = deployairgap.EnsureAgentConfig(exe.agent.Config) + deployairgap.ResolveAgentDeployment(exe.agent.Config, exe.agent.Package.Container.Image) - if useContainer { - exe.agent.Config.DeploymentType = iutil.MakeStrPtr("container") - // Use NewRemoteContainerAgent - agent, err = install.NewRemoteContainerAgent( - exe.agent.SSH.User, - exe.agent.Host, - exe.agent.SSH.Port, - exe.agent.SSH.KeyFile, - exe.agent.Name, - exe.agent.UUID, - exe.agent.Config.TimeZone, - ) - } else { - // Use NewRemoteAgent for "native" deployment type - exe.agent.Config.DeploymentType = iutil.MakeStrPtr("native") - agent, err = install.NewRemoteAgent( - exe.agent.SSH.User, - exe.agent.Host, - exe.agent.SSH.Port, - exe.agent.SSH.KeyFile, - exe.agent.Name, - exe.agent.UUID, - ) - } + edgelet, err := exe.getEdgelet() if err != nil { return err } - // Set custom scripts - if exe.agent.Scripts != nil { - if err := agent.CustomizeProcedures( - exe.agent.Scripts.Directory, - &exe.agent.Scripts.AgentProcedures); err != nil { - return err - } + if err := customizeEdgeletProcedures(edgelet, exe.agent.Scripts); err != nil { + return err } - if exe.agent.Package.Container.Image != "" { - // Set Image - agent.SetContainerImage(exe.agent.Package.Container.Image) - - } else if exe.agent.Package.Version != "" { - // Set version - agent.SetVersion(exe.agent.Package.Version) + if err := applyEdgeletPackage(edgelet, exe.agent.Package); err != nil { + return err } - // // Set version - // agent.SetVersion(exe.agent.Package.Version) - // agent.SetRepository(exe.agent.Package.Repo, exe.agent.Package.Token) - - // Handle airgap deployment if enabled - if exe.agent.Airgap { - // Validate airgap requirements - if err := deployairgap.ValidateAirgapRequirements(exe.agent.Config); err != nil { - return fmt.Errorf("airgap deployment requires valid configuration: %w", err) - } - // Resolve platform and container engine - platform, err := deployairgap.ResolvePlatform(exe.agent.Config.FogType) - if err != nil { - return fmt.Errorf("failed to resolve platform: %w", err) - } + var pendingNativeAirgap *deployairgap.AgentAirgapResult - engine, err := deployairgap.ResolveContainerEngine(exe.agent.Config.AgentConfiguration.ContainerEngine) - if err != nil { - return fmt.Errorf("failed to resolve container engine: %w", err) + if exe.agent.Airgap { + var remoteControlPlane *rsc.RemoteControlPlane + if remoteCP, ok := controlPlane.(*rsc.RemoteControlPlane); ok { + remoteControlPlane = remoteCP } - // Determine if this is initial deployment isInitial, err := deployairgap.IsInitialDeployment(exe.namespace) if err != nil { return fmt.Errorf("failed to determine deployment type: %w", err) } - - // Collect required images - // For Kubernetes control planes, pass nil and always fetch from catalog (existing control plane) - var remoteControlPlane *rsc.RemoteControlPlane - if remoteCP, ok := controlPlane.(*rsc.RemoteControlPlane); ok { - remoteControlPlane = remoteCP - } else { - // For Kubernetes or other control planes, treat as existing control plane - // and fetch images from catalog items + if remoteControlPlane == nil { isInitial = false } - images, err := deployairgap.CollectAgentImages(exe.namespace, exe.agent, remoteControlPlane, isInitial) - if err != nil { - return fmt.Errorf("failed to collect agent images: %w", err) - } - - // Get router image for the platform - routerImage, err := deployairgap.GetImageForPlatform(images, platform) + airgapPlan, err := deployairgap.PrepareAgentAirgap(exe.namespace, exe.agent, remoteControlPlane, isInitial) if err != nil { - return fmt.Errorf("failed to get router image for platform %s: %w", platform, err) + return fmt.Errorf("airgap deployment requires valid configuration: %w", err) } - // Prepare image list (agent, router for platform, NATS, debugger if available) - imageList := []string{images.Agent} - if routerImage != "" { - imageList = append(imageList, routerImage) - } - if images.Nats != "" { - imageList = append(imageList, images.Nats) - } - if images.Debugger != "" { - imageList = append(imageList, images.Debugger) - } + ctx := context.Background() + if deployairgap.IsNativeDeployment(airgapPlan.Options.DeploymentType) { + plan := airgapPlan + pendingNativeAirgap = &plan + remoteBinPath, err := deployairgap.TransferAgentAirgapBinary(ctx, exe.namespace, exe.agent.Host, &exe.agent.SSH, airgapPlan.Platform) + if err != nil { + return fmt.Errorf("failed to transfer edgelet binary: %w", err) + } + if err := edgelet.SetAirgap(remoteBinPath); err != nil { + return fmt.Errorf("failed to configure edgelet airgap binary: %w", err) + } + } else if len(airgapPlan.ImageList) > 0 { + if err := deployairgap.TransferAgentAirgapImages(ctx, exe.namespace, exe.agent.Host, &exe.agent.SSH, airgapPlan.Platform, airgapPlan.Options, airgapPlan.ImageList); err != nil { + return fmt.Errorf("failed to transfer airgap images: %w", err) + } + } + } + + if err := edgelet.Bootstrap(); err != nil { + return err + } - // Transfer images before bootstrap + if pendingNativeAirgap != nil && len(pendingNativeAirgap.ImageList) > 0 { ctx := context.Background() - if err := deployairgap.TransferAirgapImages(ctx, exe.namespace, exe.agent.Host, &exe.agent.SSH, platform, engine, imageList); err != nil { + if err := deployairgap.TransferAgentAirgapImages(ctx, exe.namespace, exe.agent.Host, &exe.agent.SSH, pendingNativeAirgap.Platform, pendingNativeAirgap.Options, pendingNativeAirgap.ImageList); err != nil { return fmt.Errorf("failed to transfer airgap images: %w", err) } } - // Try the deploy - err = agent.Bootstrap() - if err != nil { - return - } - - uuid, err := exe.ProvisionAgent() - if err != nil { - return err - } - - // Return the Agent through pointer - exe.agent.UUID = uuid - exe.agent.Created = util.NowUTC() return nil } diff --git a/internal/deploy/agentconfig/defaults.go b/internal/deploy/agentconfig/defaults.go new file mode 100644 index 000000000..8ab30211a --- /dev/null +++ b/internal/deploy/agentconfig/defaults.go @@ -0,0 +1,179 @@ +package deployagentconfig + +import ( + "fmt" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + iutil "github.com/eclipse-iofog/iofogctl/internal/util" + "github.com/eclipse-iofog/iofogctl/pkg/iofog" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const ( + DefaultMessagingPort = 5671 + DefaultEdgeRouterPort = 45671 + DefaultInterRouterPort = 55671 + DefaultNatsServerPort = 4222 + DefaultNatsClusterPort = 6222 + DefaultNatsLeafPort = 7422 + DefaultNatsMqttPort = 8883 + DefaultNatsHTTPPort = 8222 + DefaultJsStorageSize = "10G" + DefaultJsMemoryStoreSize = "1G" +) + +// PrepareForControllerAPI applies role-specific defaults and syncs arch onto the embedded SDK config. +func PrepareForControllerAPI(cfg *rsc.AgentConfiguration) { + if cfg == nil { + return + } + if iutil.IsSystemAgent(cfg) { + ApplySystemAgentDefaults(cfg) + } else { + ApplyEdgeAgentDefaults(cfg) + } + syncArchID(cfg) +} + +// ApplySystemAgentDefaults enforces interior router and server NATS with minimum port config. +func ApplySystemAgentDefaults(cfg *rsc.AgentConfiguration) { + interior := iofog.RouterModeInterior + cfg.RouterMode = &interior + + if cfg.EdgeRouterPort == nil { + cfg.EdgeRouterPort = iutil.MakeIntPtr(DefaultEdgeRouterPort) + } + if cfg.InterRouterPort == nil { + cfg.InterRouterPort = iutil.MakeIntPtr(DefaultInterRouterPort) + } + if cfg.MessagingPort == nil { + cfg.MessagingPort = iutil.MakeIntPtr(DefaultMessagingPort) + } + + cfg.NatsMode = iutil.MakeStrPtr(iofog.NatsModeServer) + if cfg.NatsServerPort == nil { + cfg.NatsServerPort = iutil.MakeIntPtr(DefaultNatsServerPort) + } + if cfg.NatsClusterPort == nil { + cfg.NatsClusterPort = iutil.MakeIntPtr(DefaultNatsClusterPort) + } + if cfg.NatsLeafPort == nil { + cfg.NatsLeafPort = iutil.MakeIntPtr(DefaultNatsLeafPort) + } + if cfg.NatsMqttPort == nil { + cfg.NatsMqttPort = iutil.MakeIntPtr(DefaultNatsMqttPort) + } + if cfg.NatsHTTPPort == nil { + cfg.NatsHTTPPort = iutil.MakeIntPtr(DefaultNatsHTTPPort) + } + if cfg.JsStorageSize == nil { + cfg.JsStorageSize = iutil.MakeStrPtr(DefaultJsStorageSize) + } + if cfg.JsMemoryStoreSize == nil { + cfg.JsMemoryStoreSize = iutil.MakeStrPtr(DefaultJsMemoryStoreSize) + } +} + +// ApplyEdgeAgentDefaults enforces edge router and leaf NATS defaults aligned with Controller behavior. +func ApplyEdgeAgentDefaults(cfg *rsc.AgentConfiguration) { + if cfg.RouterMode == nil { + edge := string(EdgeRouter) + cfg.RouterMode = &edge + } + if cfg.MessagingPort == nil { + cfg.MessagingPort = iutil.MakeIntPtr(DefaultMessagingPort) + } + + if cfg.NatsMode == nil { + leaf := string(NatsLeaf) + cfg.NatsMode = &leaf + } + if cfg.NatsServerPort == nil { + cfg.NatsServerPort = iutil.MakeIntPtr(DefaultNatsServerPort) + } + if cfg.NatsLeafPort == nil { + cfg.NatsLeafPort = iutil.MakeIntPtr(DefaultNatsLeafPort) + } + if cfg.NatsMqttPort == nil { + cfg.NatsMqttPort = iutil.MakeIntPtr(DefaultNatsMqttPort) + } + if cfg.NatsHTTPPort == nil { + cfg.NatsHTTPPort = iutil.MakeIntPtr(DefaultNatsHTTPPort) + } + if cfg.JsStorageSize == nil { + cfg.JsStorageSize = iutil.MakeStrPtr(DefaultJsStorageSize) + } + if cfg.JsMemoryStoreSize == nil { + cfg.JsMemoryStoreSize = iutil.MakeStrPtr(DefaultJsMemoryStoreSize) + } +} + +func syncArchID(cfg *rsc.AgentConfiguration) { + if cfg.Arch == nil { + return + } + arch, found := rsc.ArchStringToID(*cfg.Arch) + if !found { + arch = 0 + } + cfg.ArchID = &arch +} + +// DefaultRouterConfig returns the minimum interior router config for system agents. +func DefaultRouterConfig() client.RouterConfig { + return client.RouterConfig{ + RouterMode: iutil.MakeStrPtr(iofog.RouterModeInterior), + MessagingPort: iutil.MakeIntPtr(DefaultMessagingPort), + EdgeRouterPort: iutil.MakeIntPtr(DefaultEdgeRouterPort), + InterRouterPort: iutil.MakeIntPtr(DefaultInterRouterPort), + } +} + +func validateSystemAgent(config *rsc.AgentConfiguration) error { + name := config.Name + if config.RouterMode != nil && *config.RouterMode != iofog.RouterModeInterior { + return util.NewInputError(fmt.Sprintf( + "agent config %s validation failed. System agent routerMode must be interior", name)) + } + if config.NatsMode != nil && *config.NatsMode != iofog.NatsModeServer { + return util.NewInputError(fmt.Sprintf( + "agent config %s validation failed. System agent natsMode must be server", name)) + } + return validateRouterNatsFields(config, InteriorRouter, NatsServer) +} + +func validateEdgeAgent(config *rsc.AgentConfiguration) error { + routerMode := getRouterMode(config) + natsMode := getNatsMode(config) + + if routerMode != EdgeRouter && routerMode != InteriorRouter && routerMode != NoneRouter { + msg := "agent config %s validation failed. RouterMode has to be one of edge, interior, none. Default is: edge" + return util.NewInputError(fmt.Sprintf(msg, config.Name)) + } + if natsMode != NatsServer && natsMode != NatsLeaf && natsMode != NatsNone { + msg := "agent config %s validation failed. NatsMode has to be one of leaf, server, none. Default is: leaf" + return util.NewInputError(fmt.Sprintf(msg, config.Name)) + } + return validateRouterNatsFields(config, routerMode, natsMode) +} + +func validateRouterNatsFields(config *rsc.AgentConfiguration, routerMode RouterMode, natsMode NatsMode) error { + if routerMode != NoneRouter && config.NetworkRouter != nil { + msg := "agent config %s validation failed. Cannot have a network if routerMode is different from none. Current router mode is: %s" + return util.NewInputError(fmt.Sprintf(msg, config.Name, routerMode)) + } + if routerMode == NoneRouter && config.UpstreamRouters != nil && len(*config.UpstreamRouters) > 0 { + msg := "agent config %s validation failed. Cannot have a upstreamRouters if routerMode is none" + return util.NewInputError(fmt.Sprintf(msg, config.Name)) + } + if routerMode != InteriorRouter && (config.EdgeRouterPort != nil || config.InterRouterPort != nil) { + msg := "agent config %s validation failed. Cannot have an edgeRouterPort or interRouterPort if routerMode is different from interior. Current router mode is: %s" + return util.NewInputError(fmt.Sprintf(msg, config.Name, routerMode)) + } + if natsMode != NatsServer && config.NatsClusterPort != nil { + msg := "agent config %s validation failed. Cannot have a natsClusterPort if natsMode is different from server" + return util.NewInputError(fmt.Sprintf(msg, config.Name)) + } + return nil +} diff --git a/internal/deploy/agentconfig/factory.go b/internal/deploy/agentconfig/factory.go index b9ccd0173..6eea4414a 100644 --- a/internal/deploy/agentconfig/factory.go +++ b/internal/deploy/agentconfig/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployagentconfig import ( @@ -113,7 +100,7 @@ func isOverridingSystemAgent(controllerHost, agentHost, agentName string, isSyst return err } } - if agentURL.Hostname() == controllerURL.Hostname() && isSystem == false && agentName != iofog.VanillaLocalAgentName { + if agentURL.Hostname() == controllerURL.Hostname() && !isSystem && agentName != iofog.VanillaLocalAgentName { return util.NewConflictError("Cannot deploy an agent on the same host than the Controller\n") } return nil @@ -197,13 +184,20 @@ func (exe *RemoteExecutor) Execute() error { return err } exe.uuid = uuid - return nil + } else { + // Update existing Agent + exe.uuid = agent.UUID + if err = clientutil.ExecuteWithAuthRetry(exe.namespace, func(ctrlClient *client.Client) error { + return updateAgentConfiguration(exe.agentConfig, exe.tags, agent.UUID, ctrlClient) + }); err != nil { + return err + } } - // Update existing Agent - exe.uuid = agent.UUID - return clientutil.ExecuteWithAuthRetry(exe.namespace, func(ctrlClient *client.Client) error { - return updateAgentConfiguration(exe.agentConfig, exe.tags, agent.UUID, ctrlClient) - }) + + if !isSystem || install.IsVerbose() { + util.SpinStart(fmt.Sprintf("Waiting for agent %s platform ready", exe.GetName())) + } + return clientutil.WaitForAgentPlatformReadyWithRetry(exe.namespace, exe.uuid) } func NewExecutor(opt Options) (exe execute.Executor, err error) { diff --git a/internal/deploy/agentconfig/utils.go b/internal/deploy/agentconfig/utils.go index 22b561e4e..af4746d26 100644 --- a/internal/deploy/agentconfig/utils.go +++ b/internal/deploy/agentconfig/utils.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployagentconfig import ( @@ -18,6 +5,7 @@ import ( "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + iutil "github.com/eclipse-iofog/iofogctl/internal/util" "github.com/eclipse-iofog/iofogctl/pkg/iofog" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -36,45 +24,27 @@ const ( ) func getRouterMode(config *rsc.AgentConfiguration) RouterMode { - if config.RouterConfig.RouterMode != nil { - return RouterMode(*config.RouterConfig.RouterMode) + if config.RouterMode != nil { + return RouterMode(*config.RouterMode) } return EdgeRouter } func getNatsMode(config *rsc.AgentConfiguration) NatsMode { - if config.NatsConfig.NatsMode != nil { - return NatsMode(*config.NatsConfig.NatsMode) + if config.NatsMode != nil { + return NatsMode(*config.NatsMode) } return NatsLeaf } func Validate(config *rsc.AgentConfiguration) error { - routerMode := getRouterMode(config) - natsMode := getNatsMode(config) - - if routerMode != EdgeRouter && routerMode != InteriorRouter && routerMode != NoneRouter { - msg := "agent config %s validation failed. RouterMode has to be one of edge, interior, none. Default is: edge" - return util.NewInputError(fmt.Sprintf(msg, config.Name)) - } - if routerMode != NoneRouter && config.NetworkRouter != nil { - msg := "agent config %s validation failed. Cannot have a network if routerMode is different from none. Current router mode is: %s" - return util.NewInputError(fmt.Sprintf(msg, config.Name, routerMode)) - } - if routerMode == NoneRouter && config.UpstreamRouters != nil && len(*config.UpstreamRouters) > 0 { - msg := "agent config %s validation failed. Cannot have a upstreamRouters if routerMode is none" - return util.NewInputError(fmt.Sprintf(msg, config.Name)) + if config == nil { + return nil } - if routerMode != InteriorRouter && (config.RouterConfig.EdgeRouterPort != nil || config.RouterConfig.InterRouterPort != nil) { - msg := "agent config %s validation failed. Cannot have an edgeRouterPort or interRouterPort if routerMode is different from interior. Current router mode is: %s" - return util.NewInputError(fmt.Sprintf(msg, config.Name, routerMode)) + if iutil.IsSystemAgent(config) { + return validateSystemAgent(config) } - if natsMode != NatsServer && (config.NatsConfig.NatsClusterPort != nil) { - msg := "agent config %s validation failed. Cannot have a natsClusterPort if natsMode is different from server" - return util.NewInputError(fmt.Sprintf(msg, config.Name)) - } - - return nil + return validateEdgeAgent(config) } func findAgentUUIDInList(list []client.AgentInfo, name string) (uuid string, err error) { @@ -138,36 +108,37 @@ func Process(agentConfig *rsc.AgentConfiguration, name, agentIP string, otherAge } func getAgentUpdateRequestFromAgentConfig(agentConfig *rsc.AgentConfiguration, tags *[]string) (request client.AgentUpdateRequest) { - var fogTypePtr *int64 - if agentConfig.FogType != nil { - fogType, found := rsc.FogTypeStringMap[*agentConfig.FogType] - if !found { - fogType = 0 - } - fogTypePtr = &fogType - } request.Location = agentConfig.Location request.Latitude = agentConfig.Latitude request.Longitude = agentConfig.Longitude request.Description = agentConfig.Description request.Name = agentConfig.Name - request.FogType = fogTypePtr request.AgentConfiguration = agentConfig.AgentConfiguration request.Tags = tags + if agentConfig.ArchID != nil { + request.ArchID = agentConfig.ArchID + } else if agentConfig.Arch != nil { + arch, found := rsc.ArchStringToID(*agentConfig.Arch) + if !found { + arch = 0 + } + request.ArchID = &arch + } return } func createAgentFromConfiguration(agentConfig *rsc.AgentConfiguration, tags *[]string, name string, clt *client.Client) (uuid string, err error) { + PrepareForControllerAPI(agentConfig) updateAgentConfigRequest := getAgentUpdateRequestFromAgentConfig(agentConfig, tags) createAgentRequest := &client.CreateAgentRequest{ AgentUpdateRequest: updateAgentConfigRequest, } - if createAgentRequest.AgentUpdateRequest.Name == "" { - createAgentRequest.AgentUpdateRequest.Name = name + if createAgentRequest.Name == "" { + createAgentRequest.Name = name } - if createAgentRequest.AgentUpdateRequest.FogType == nil { - fogType := int64(0) - createAgentRequest.AgentUpdateRequest.FogType = &fogType + if createAgentRequest.ArchID == nil { + arch := int64(0) + createAgentRequest.ArchID = &arch } agent, err := clt.CreateAgent(createAgentRequest) if err != nil { @@ -178,6 +149,7 @@ func createAgentFromConfiguration(agentConfig *rsc.AgentConfiguration, tags *[]s func updateAgentConfiguration(agentConfig *rsc.AgentConfiguration, tags *[]string, uuid string, clt *client.Client) (err error) { if agentConfig != nil { + PrepareForControllerAPI(agentConfig) updateAgentConfigRequest := getAgentUpdateRequestFromAgentConfig(agentConfig, tags) updateAgentConfigRequest.UUID = uuid diff --git a/internal/deploy/agentconfig/utils_test.go b/internal/deploy/agentconfig/utils_test.go new file mode 100644 index 000000000..eccc8660d --- /dev/null +++ b/internal/deploy/agentconfig/utils_test.go @@ -0,0 +1,83 @@ +package deployagentconfig + +import ( + "encoding/json" + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + iutil "github.com/eclipse-iofog/iofogctl/internal/util" + "github.com/stretchr/testify/require" +) + +func TestGetAgentUpdateRequestPreservesArchID(t *testing.T) { + arch := "arm64" + cfg := &rsc.AgentConfiguration{ + Name: "remote-1", + Arch: &arch, + AgentConfiguration: client.AgentConfiguration{ + Host: iutil.MakeStrPtr("192.168.1.1"), + }, + } + PrepareForControllerAPI(cfg) + + req := getAgentUpdateRequestFromAgentConfig(cfg, nil) + require.NotNil(t, req.ArchID) + require.Equal(t, int64(2), *req.ArchID) + + body, err := json.Marshal(&client.CreateAgentRequest{AgentUpdateRequest: req}) + require.NoError(t, err) + require.Contains(t, string(body), `"archId":2`) +} + +func TestPrepareForControllerAPISystemAgentDefaults(t *testing.T) { + cfg := &rsc.AgentConfiguration{ + Name: "cp-system", + AgentConfiguration: client.AgentConfiguration{ + IsSystem: iutil.MakeBoolPtr(true), + }, + } + PrepareForControllerAPI(cfg) + + require.NotNil(t, cfg.RouterMode) + require.Equal(t, "interior", *cfg.RouterMode) + require.NotNil(t, cfg.NatsMode) + require.Equal(t, "server", *cfg.NatsMode) + require.NotNil(t, cfg.MessagingPort) + require.Equal(t, DefaultMessagingPort, *cfg.MessagingPort) + require.NotNil(t, cfg.NatsClusterPort) + require.Equal(t, DefaultNatsClusterPort, *cfg.NatsClusterPort) +} + +func TestPrepareForControllerAPIEdgeAgentDefaults(t *testing.T) { + cfg := &rsc.AgentConfiguration{Name: "edge-1"} + PrepareForControllerAPI(cfg) + + require.NotNil(t, cfg.RouterMode) + require.Equal(t, "edge", *cfg.RouterMode) + require.NotNil(t, cfg.NatsMode) + require.Equal(t, "leaf", *cfg.NatsMode) + require.NotNil(t, cfg.NatsLeafPort) + require.Equal(t, DefaultNatsLeafPort, *cfg.NatsLeafPort) +} + +func TestValidateSystemAgentRejectsEdgeRouter(t *testing.T) { + edge := "edge" + cfg := &rsc.AgentConfiguration{ + Name: "sys", + AgentConfiguration: client.AgentConfiguration{ + IsSystem: iutil.MakeBoolPtr(true), + RouterConfig: client.RouterConfig{ + RouterMode: &edge, + }, + }, + } + err := Validate(cfg) + require.Error(t, err) + require.Contains(t, err.Error(), "routerMode must be interior") +} + +func TestValidateEdgeAgentAllowsOmittedModes(t *testing.T) { + cfg := &rsc.AgentConfiguration{Name: "edge-1"} + require.NoError(t, Validate(cfg)) +} diff --git a/internal/deploy/airgap/agent.go b/internal/deploy/airgap/agent.go new file mode 100644 index 000000000..bc40a25e5 --- /dev/null +++ b/internal/deploy/airgap/agent.go @@ -0,0 +1,62 @@ +package deployairgap + +import ( + "context" + "fmt" + + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" +) + +// AgentAirgapResult holds artifacts produced during agent airgap preparation. +type AgentAirgapResult struct { + Platform string + Options AirgapTransferOptions + RemoteBinPath string + ImageList []string +} + +// PrepareAgentAirgap resolves platform, options, and image refs for an agent airgap deploy. +func PrepareAgentAirgap(namespace string, agent *rsc.RemoteAgent, controlPlane *rsc.RemoteControlPlane, isInitial bool) (AgentAirgapResult, error) { + if agent == nil || agent.Config == nil { + return AgentAirgapResult{}, fmt.Errorf("agent configuration is required for airgap deployment") + } + if err := ValidateAirgapRequirements(agent.Config); err != nil { + return AgentAirgapResult{}, err + } + + platform, err := ResolvePlatform(agent.Config.Arch) + if err != nil { + return AgentAirgapResult{}, fmt.Errorf("failed to resolve platform: %w", err) + } + + opts, err := AirgapTransferOptionsFromConfig(agent.Config) + if err != nil { + return AgentAirgapResult{}, fmt.Errorf("failed to resolve airgap transfer options: %w", err) + } + + images, err := CollectAgentImages(namespace, agent, controlPlane, isInitial) + if err != nil { + return AgentAirgapResult{}, fmt.Errorf("failed to collect agent images: %w", err) + } + + imageList, err := CollectAgentAirgapImages(images, platform, opts.DeploymentType) + if err != nil { + return AgentAirgapResult{}, fmt.Errorf("failed to build agent airgap image list: %w", err) + } + + return AgentAirgapResult{ + Platform: platform, + Options: opts, + ImageList: imageList, + }, nil +} + +// TransferAgentAirgapBinary caches and SCPs the edgelet binary when deploymentType is native. +func TransferAgentAirgapBinary(ctx context.Context, namespace, host string, ssh *rsc.SSH, platform string) (string, error) { + return EnsureAndTransferEdgeletBinary(ctx, namespace, host, platform, ssh) +} + +// TransferAgentAirgapImages transfers and loads container images using the airgap load matrix. +func TransferAgentAirgapImages(ctx context.Context, namespace, host string, ssh *rsc.SSH, platform string, opts AirgapTransferOptions, images []string) error { + return TransferAirgapImages(ctx, namespace, host, ssh, platform, opts, images) +} diff --git a/internal/deploy/airgap/agent_edgelet.go b/internal/deploy/airgap/agent_edgelet.go new file mode 100644 index 000000000..a30b74e79 --- /dev/null +++ b/internal/deploy/airgap/agent_edgelet.go @@ -0,0 +1,98 @@ +package deployairgap + +import ( + "runtime" + "strings" + + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + iutil "github.com/eclipse-iofog/iofogctl/internal/util" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +// EnsureAgentConfig returns a non-nil agent configuration struct. +func EnsureAgentConfig(agent *rsc.AgentConfiguration) *rsc.AgentConfiguration { + if agent == nil { + return &rsc.AgentConfiguration{} + } + return agent +} + +// ResolveAgentDeployment normalizes deployment defaults and reports whether container install is selected. +func ResolveAgentDeployment(cfg *rsc.AgentConfiguration, containerImage string) bool { + cfg = EnsureAgentConfig(cfg) + + useContainer := cfg.DeploymentType != nil && strings.EqualFold(strings.TrimSpace(*cfg.DeploymentType), DeploymentTypeContainer) + if containerImage != "" { + useContainer = true + } + + if useContainer { + cfg.DeploymentType = iutil.MakeStrPtr(DeploymentTypeContainer) + } else { + cfg.DeploymentType = iutil.MakeStrPtr(DeploymentTypeNative) + } + + if cfg.ContainerEngine == nil || strings.TrimSpace(*cfg.ContainerEngine) == "" { + cfg.ContainerEngine = iutil.MakeStrPtr(string(EngineEdgelet)) + } + + return useContainer +} + +func edgeletRuntimeSpec(cfg *rsc.AgentConfiguration) *install.EdgeletRuntimeSpec { + if cfg == nil { + return nil + } + arch := "auto" + if cfg.Arch != nil && strings.TrimSpace(*cfg.Arch) != "" { + arch = strings.TrimSpace(*cfg.Arch) + } + return &install.EdgeletRuntimeSpec{ + Arch: arch, + Latitude: cfg.Latitude, + Longitude: cfg.Longitude, + Agent: cfg.AgentConfiguration, + } +} + +// EdgeletInstallConfig builds layered install settings from agent YAML spec fields. +func EdgeletInstallConfig(hostOS string, cfg *rsc.AgentConfiguration, pkg rsc.Package) install.EdgeletInstallConfig { + cfg = EnsureAgentConfig(cfg) + ResolveAgentDeployment(cfg, pkg.Container.Image) + + arch := "auto" + if cfg.Arch != nil && strings.TrimSpace(*cfg.Arch) != "" { + arch = strings.TrimSpace(*cfg.Arch) + } + + engine := string(EngineEdgelet) + if cfg.ContainerEngine != nil && strings.TrimSpace(*cfg.ContainerEngine) != "" { + engine = strings.TrimSpace(*cfg.ContainerEngine) + } + + deploymentType := ResolveDeploymentType(cfg.DeploymentType) + + installCfg := install.EdgeletInstallConfig{ + HostOS: hostOS, + Arch: arch, + ContainerEngine: engine, + DeploymentType: deploymentType, + ContainerImage: pkg.Container.Image, + TimeZone: cfg.TimeZone, + Runtime: edgeletRuntimeSpec(cfg), + } + if pkg.Version != "" { + installCfg.Version = pkg.Version + } + return installCfg +} + +// LocalEdgeletHostOS returns the normalized edgelet OS name for the local runtime. +func LocalEdgeletHostOS() string { + osName, err := util.NormalizeEdgeletOS(runtime.GOOS) + if err != nil { + return "linux" + } + return osName +} diff --git a/internal/deploy/airgap/binary.go b/internal/deploy/airgap/binary.go new file mode 100644 index 000000000..8e7b4def2 --- /dev/null +++ b/internal/deploy/airgap/binary.go @@ -0,0 +1,177 @@ +package deployairgap + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/eclipse-iofog/iofogctl/internal/config" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const binaryMetadataFilename = "metadata.json" + +type binaryCacheMetadata struct { + OS string `json:"os"` + Arch string `json:"arch"` + Version string `json:"version"` + Checksum string `json:"checksum,omitempty"` + Size int64 `json:"size,omitempty"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// EnsureEdgeletBinary downloads or reuses a cached edgelet release binary. +func EnsureEdgeletBinary(_ context.Context, namespace, osName, archName string) (string, error) { + localPath := config.GetAirgapBinaryCachePath(namespace, osName, archName) + cacheDir := filepath.Dir(localPath) + if err := os.MkdirAll(cacheDir, util.DirPerm); err != nil { + return "", err + } + + version := util.GetEdgeletBinaryVersion() + metaPath := filepath.Join(cacheDir, binaryMetadataFilename) + if cached, err := loadBinaryCacheMetadata(metaPath); err == nil { + if cached.Version == version && cached.OS == osName && cached.Arch == archName { + if ok, reason := canReuseCachedBinary(localPath, cached); ok { + util.PrintInfo(fmt.Sprintf("Reusing cached edgelet binary for %s/%s", osName, archName)) + return localPath, nil + } else if reason != "" { + util.PrintNotify(reason) + } + } + } + + util.PrintInfo(fmt.Sprintf("Downloading edgelet binary for %s/%s", osName, archName)) + if err := util.DownloadEdgeletBinary(osName, archName, localPath); err != nil { + return "", fmt.Errorf("failed to download edgelet binary: %w", err) + } + + checksum, size, err := calculateFileChecksum(localPath) + if err != nil { + return "", err + } + if err := saveBinaryCacheMetadata(metaPath, binaryCacheMetadata{ + OS: osName, + Arch: archName, + Version: version, + Checksum: checksum, + Size: size, + UpdatedAt: time.Now().UTC(), + }); err != nil { + return "", err + } + return localPath, nil +} + +func loadBinaryCacheMetadata(path string) (*binaryCacheMetadata, error) { + data, err := util.ReadValidatedFile(path) + if err != nil { + return nil, err + } + var meta binaryCacheMetadata + if err := json.Unmarshal(data, &meta); err != nil { + return nil, err + } + return &meta, nil +} + +func saveBinaryCacheMetadata(path string, meta binaryCacheMetadata) error { + data, err := json.Marshal(meta) + if err != nil { + return err + } + return util.WriteValidatedFile(path, data, util.FilePerm) +} + +func canReuseCachedBinary(path string, cached *binaryCacheMetadata) (bool, string) { + info, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return false, fmt.Sprintf("Cached edgelet binary for %s/%s is missing on disk; refreshing cache", cached.OS, cached.Arch) + } + return false, fmt.Sprintf("Failed to stat cached edgelet binary %s: %v", path, err) + } + if cached.Checksum == "" { + return false, "Cached edgelet binary is missing checksum metadata; refreshing cache" + } + checksum, size, err := calculateFileChecksum(path) + if err != nil { + return false, fmt.Sprintf("Failed to verify cached edgelet binary: %v", err) + } + if checksum != cached.Checksum { + return false, "Cached edgelet binary checksum mismatch; refreshing cache" + } + if size != cached.Size || info.Size() != cached.Size { + return false, "Cached edgelet binary size mismatch; refreshing cache" + } + return true, "" +} + +// TransferAirgapBinary SCPs a raw edgelet binary to a remote host and returns the remote path. +func TransferAirgapBinary(host string, ssh *rsc.SSH, osName, archName, localPath string) (string, error) { + if host == "" { + return "", util.NewInputError("host is required for airgap binary transfer") + } + if ssh == nil || ssh.User == "" || ssh.KeyFile == "" { + return "", util.NewInputError("SSH configuration is required for airgap binary transfer") + } + if localPath == "" { + return "", util.NewInputError("local binary path is required for airgap binary transfer") + } + + filename, err := util.EdgeletBinaryArtifact(osName, archName) + if err != nil { + return "", err + } + + client, err := util.NewSecureShellClient(ssh.User, host, ssh.KeyFile) + if err != nil { + return "", err + } + client.SetPort(ssh.Port) + if err := client.Connect(); err != nil { + return "", err + } + defer util.Log(client.Disconnect) + + hostDir := util.JoinAgentPath(remoteAirgapDir, SanitizeSegment(host)) + if err := client.CreateFolder(hostDir); err != nil { + return "", err + } + + file, err := util.OpenValidatedFile(localPath) + if err != nil { + return "", err + } + defer file.Close() + + info, err := file.Stat() + if err != nil { + return "", err + } + + if err := client.CopyTo(file, util.AddTrailingSlash(hostDir), filename, "0700", info.Size()); err != nil { + return "", err + } + + remotePath := util.JoinAgentPath(hostDir, filename) + util.PrintInfo(fmt.Sprintf("Edgelet binary transfer to %s complete", host)) + return remotePath, nil +} + +// EnsureAndTransferEdgeletBinary caches, then SCPs, an edgelet binary for the given platform. +func EnsureAndTransferEdgeletBinary(ctx context.Context, namespace, host, platform string, ssh *rsc.SSH) (string, error) { + osName, archName, err := PlatformToOSArch(platform) + if err != nil { + return "", err + } + localPath, err := EnsureEdgeletBinary(ctx, namespace, osName, archName) + if err != nil { + return "", err + } + return TransferAirgapBinary(host, ssh, osName, archName, localPath) +} diff --git a/internal/deploy/airgap/binary_test.go b/internal/deploy/airgap/binary_test.go new file mode 100644 index 000000000..e55f0f5c7 --- /dev/null +++ b/internal/deploy/airgap/binary_test.go @@ -0,0 +1,73 @@ +package deployairgap + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func TestEnsureEdgeletBinaryUsesCache(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1.0.0-rc.6/edgelet-linux-amd64" { + http.NotFound(w, r) + return + } + _, _ = w.Write([]byte("cached-edgelet-binary")) + })) + defer server.Close() + + util.SetEdgeletReleaseBaseForTest(server.URL) + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") + t.Cleanup(func() { + util.ResetEdgeletReleaseBaseForTest() + util.ResetEdgeletBinaryVersionForTest() + }) + + config.Init(t.TempDir()) + namespace := "default" + + first, err := EnsureEdgeletBinary(context.Background(), namespace, "linux", "amd64") + if err != nil { + t.Fatalf("EnsureEdgeletBinary first: %v", err) + } + data, err := os.ReadFile(first) + if err != nil { + t.Fatalf("read cached binary: %v", err) + } + if string(data) != "cached-edgelet-binary" { + t.Fatalf("unexpected binary contents: %q", string(data)) + } + + second, err := EnsureEdgeletBinary(context.Background(), namespace, "linux", "amd64") + if err != nil { + t.Fatalf("EnsureEdgeletBinary second: %v", err) + } + if second != first { + t.Fatalf("expected cache reuse at %q, got %q", first, second) + } + + wantPath := config.GetAirgapBinaryCachePath(namespace, "linux", "amd64") + if first != wantPath { + t.Fatalf("cache path = %q, want %q", first, wantPath) + } + + metaPath := filepath.Join(filepath.Dir(first), binaryMetadataFilename) + if _, err := os.Stat(metaPath); err != nil { + t.Fatalf("metadata file missing: %v", err) + } +} + +func TestGetAirgapBinaryCachePathWindows(t *testing.T) { + config.Init(t.TempDir()) + got := config.GetAirgapBinaryCachePath("default", "windows", "amd64") + if !strings.Contains(got, "edgelet-windows-amd64.exe") { + t.Fatalf("cache path = %q, want windows executable artifact name", got) + } +} diff --git a/internal/deploy/airgap/cache.go b/internal/deploy/airgap/cache.go index 024ca7bcb..be7f6e975 100644 --- a/internal/deploy/airgap/cache.go +++ b/internal/deploy/airgap/cache.go @@ -7,9 +7,11 @@ import ( "os" "time" - "github.com/containers/image/v5/manifest" - "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" + "go.podman.io/image/v5/manifest" + "go.podman.io/image/v5/types" + + "github.com/eclipse-iofog/iofogctl/pkg/util" ) const cacheMetadataFilename = "metadata.json" @@ -56,7 +58,7 @@ func fetchRemoteDigest(ctx context.Context, imageRef string, sysCtx *types.Syste } func loadCacheMetadata(path string) (*cacheMetadata, error) { - data, err := os.ReadFile(path) + data, err := util.ReadValidatedFile(path) if err != nil { return nil, err } @@ -72,7 +74,7 @@ func saveCacheMetadata(path string, meta cacheMetadata) error { if err != nil { return err } - return os.WriteFile(path, data, 0o644) + return util.WriteValidatedFile(path, data, util.FilePerm) } func canReuseCachedArtifact(archivePath string, meta *cacheMetadata, imageRef, platform, digestValue string) (bool, string) { diff --git a/internal/deploy/airgap/helpers.go b/internal/deploy/airgap/helpers.go index 1e29e0818..c3efa1c84 100644 --- a/internal/deploy/airgap/helpers.go +++ b/internal/deploy/airgap/helpers.go @@ -1,6 +1,7 @@ package deployairgap import ( + "fmt" "strings" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" @@ -8,42 +9,75 @@ import ( ) const ( - PlatformAMD64 = "linux/amd64" - PlatformARM64 = "linux/arm64" + PlatformAMD64 = "linux/amd64" + PlatformARM64 = "linux/arm64" + PlatformRISCV64 = "linux/riscv64" + PlatformARM = "linux/arm" + + DeploymentTypeNative = "native" + DeploymentTypeContainer = "container" ) type ContainerEngine string const ( - EngineDocker ContainerEngine = "docker" - EnginePodman ContainerEngine = "podman" + EngineEdgelet ContainerEngine = "edgelet" + EngineDocker ContainerEngine = "docker" + EnginePodman ContainerEngine = "podman" ) func (e ContainerEngine) Command() string { return string(e) } -func ResolvePlatform(fogType *string) (string, error) { - if fogType == nil { +// AirgapTransferOptions selects image load commands for an airgap transfer. +type AirgapTransferOptions struct { + DeploymentType string + Engine ContainerEngine +} + +func ResolvePlatform(arch *string) (string, error) { + if arch == nil { return "", util.NewInputError("Agent fog type is not configured") } - value := strings.ToLower(strings.TrimSpace(*fogType)) + value := strings.ToLower(strings.TrimSpace(*arch)) switch value { case "1", "x86", "amd64", PlatformAMD64: return PlatformAMD64, nil case "2", "arm", "arm64", PlatformARM64: return PlatformARM64, nil + case "3", "riscv64", PlatformRISCV64: + return PlatformRISCV64, nil + default: + return "", util.NewInputError("Unsupported fog type " + *arch) + } +} + +func ResolveDeploymentType(deploymentType *string) string { + if deploymentType == nil { + return DeploymentTypeNative + } + value := strings.ToLower(strings.TrimSpace(*deploymentType)) + switch value { + case DeploymentTypeContainer: + return DeploymentTypeContainer default: - return "", util.NewInputError("Unsupported fog type " + *fogType) + return DeploymentTypeNative } } +func IsNativeDeployment(deploymentType string) bool { + return ResolveDeploymentType(&deploymentType) == DeploymentTypeNative +} + func ResolveContainerEngine(engine *string) (ContainerEngine, error) { if engine == nil { return "", util.NewInputError("Agent container engine configuration is missing") } value := strings.ToLower(strings.TrimSpace(*engine)) switch value { + case "edgelet": + return EngineEdgelet, nil case "docker": return EngineDocker, nil case "podman": @@ -53,6 +87,105 @@ func ResolveContainerEngine(engine *string) (ContainerEngine, error) { } } +// AirgapTransferOptionsFromConfig resolves deployment type and engine defaults for airgap transfers. +func AirgapTransferOptionsFromConfig(cfg *rsc.AgentConfiguration) (AirgapTransferOptions, error) { + if cfg == nil { + return AirgapTransferOptions{}, util.NewInputError("Agent configuration is required for airgap deployment") + } + deploymentType := ResolveDeploymentType(cfg.DeploymentType) + engineStr := "" + if cfg.ContainerEngine != nil { + engineStr = strings.ToLower(strings.TrimSpace(*cfg.ContainerEngine)) + } + if engineStr == "" { + if deploymentType == DeploymentTypeNative { + return AirgapTransferOptions{DeploymentType: deploymentType, Engine: EngineEdgelet}, nil + } + return AirgapTransferOptions{}, util.NewInputError("ContainerEngine is required for container airgap deployment") + } + engine, err := ResolveContainerEngine(&engineStr) + if err != nil { + return AirgapTransferOptions{}, err + } + return AirgapTransferOptions{DeploymentType: deploymentType, Engine: engine}, nil +} + +// PlatformToOSArch splits a platform string such as linux/amd64 into OS and arch parts. +func PlatformToOSArch(platform string) (osName, archName string, err error) { + parts := strings.Split(platform, "/") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", util.NewInternalError("invalid platform specification " + platform) + } + return parts[0], parts[1], nil +} + +// ImageLoadCommand builds the remote shell command used to import a transferred image archive. +func ImageLoadCommand(opts AirgapTransferOptions, remoteArchivePath string) string { + if IsNativeDeployment(opts.DeploymentType) && opts.Engine == EngineEdgelet { + tarPath := strings.TrimSuffix(remoteArchivePath, ".gz") + return fmt.Sprintf( + `sudo sh -c 'gunzip -c %q > %q && edgelet image load -f %q && rm -f %q'`, + remoteArchivePath, tarPath, tarPath, tarPath, + ) + } + return fmt.Sprintf("sudo -S %s load -i %s", opts.Engine.Command(), remoteArchivePath) +} + +// CollectAgentAirgapImages returns image refs to transfer for an agent airgap deploy. +// Native deployments skip the edgelet container image because the raw binary is transferred separately. +func CollectAgentAirgapImages(images *RequiredImages, platform, deploymentType string) ([]string, error) { + if images == nil { + return nil, util.NewInternalError("required images are missing") + } + + imageList := make([]string, 0, 8) + if !IsNativeDeployment(deploymentType) && images.Agent != "" { + imageList = append(imageList, images.Agent) + } + + routerImage, err := GetImageForPlatform(images, platform) + if err != nil { + return nil, err + } + if routerImage != "" { + imageList = append(imageList, routerImage) + } + + for _, ref := range []string{ + images.NatsAMD64, + images.DebuggerAMD64, + images.NatsARM64, + images.DebuggerARM64, + images.NatsRISCV64, + images.DebuggerRISCV64, + images.NatsARM, + images.DebuggerARM, + } { + if ref != "" { + imageList = append(imageList, ref) + } + } + return imageList, nil +} + +// ControllerAirgapLoadOptions returns docker/podman load options for controller container images. +func ControllerAirgapLoadOptions(cfg *rsc.AgentConfiguration) (AirgapTransferOptions, error) { + if cfg == nil || cfg.ContainerEngine == nil { + return AirgapTransferOptions{}, util.NewInputError("containerEngine docker or podman is required to load controller images in airgap mode") + } + engineStr := strings.ToLower(strings.TrimSpace(*cfg.ContainerEngine)) + switch engineStr { + case "docker", "podman": + engine, err := ResolveContainerEngine(&engineStr) + if err != nil { + return AirgapTransferOptions{}, err + } + return AirgapTransferOptions{DeploymentType: DeploymentTypeContainer, Engine: engine}, nil + default: + return AirgapTransferOptions{}, util.NewInputError("controller airgap image load requires containerEngine docker or podman on the controller host") + } +} + func SanitizeSegment(value string) string { if value == "" { return "default" @@ -75,27 +208,40 @@ func SanitizeSegment(value string) string { return result } -// ValidateAirgapRequirements validates that required configuration is present for airgap deployment +// ValidateAirgapRequirements validates that required configuration is present for airgap deployment. func ValidateAirgapRequirements(agentConfig *rsc.AgentConfiguration) error { if agentConfig == nil { return util.NewInputError("Agent configuration is required for airgap deployment") } - // Validate FogType - if agentConfig.FogType == nil || *agentConfig.FogType == "" { - return util.NewInputError("FogType is required for airgap deployment. Please specify the agent architecture (x86 or arm)") + if agentConfig.Arch == nil || *agentConfig.Arch == "" { + return util.NewInputError("Arch is required for airgap deployment. Please specify the agent architecture (x86 or arm)") } - // Validate ContainerEngine - if agentConfig.AgentConfiguration.ContainerEngine == nil || *agentConfig.AgentConfiguration.ContainerEngine == "" { - return util.NewInputError("ContainerEngine is required for airgap deployment. Please specify the container engine (docker or podman)") + deploymentType := ResolveDeploymentType(agentConfig.DeploymentType) + engineStr := "" + if agentConfig.ContainerEngine != nil { + engineStr = strings.ToLower(strings.TrimSpace(*agentConfig.ContainerEngine)) } - return nil + if deploymentType == DeploymentTypeContainer { + if engineStr != "docker" && engineStr != "podman" { + return util.NewInputError("ContainerEngine docker or podman is required for container airgap deployment") + } + return nil + } + + if engineStr == "" || engineStr == "edgelet" { + return nil + } + if engineStr == "docker" || engineStr == "podman" { + return nil + } + return util.NewInputError("Unsupported container engine " + engineStr + " for native airgap deployment") } // ValidateControlPlaneAirgapRequirements validates that each controller has system agent config with -// agent type (FogType) and container engine when airgap is enabled. Router and debugger are transferred +// agent type (Arch) and container engine when airgap is enabled. Router and debugger are transferred // only in the system agent phase, so system agent config is required to resolve platform. func ValidateControlPlaneAirgapRequirements(controlPlane *rsc.RemoteControlPlane) error { if controlPlane == nil { @@ -103,7 +249,7 @@ func ValidateControlPlaneAirgapRequirements(controlPlane *rsc.RemoteControlPlane } for _, ctrl := range controlPlane.Controllers { if ctrl.SystemAgent == nil || ctrl.SystemAgent.AgentConfiguration == nil { - return util.NewInputError("System agent configuration is required for airgap control plane deployment. Please specify systemAgent with agent type (x86 or arm) and container engine (docker or podman) for controller " + ctrl.Name) + return util.NewInputError("System agent configuration is required for airgap control plane deployment. Please specify systemAgent with agent type (x86 or arm) and container engine for controller " + ctrl.Name) } if err := ValidateAirgapRequirements(ctrl.SystemAgent.AgentConfiguration); err != nil { return err diff --git a/internal/deploy/airgap/helpers_test.go b/internal/deploy/airgap/helpers_test.go new file mode 100644 index 000000000..5f035f01f --- /dev/null +++ b/internal/deploy/airgap/helpers_test.go @@ -0,0 +1,223 @@ +package deployairgap + +import ( + "strings" + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" +) + +func strPtr(v string) *string { return &v } + +func TestImageLoadCommand(t *testing.T) { + tests := []struct { + name string + opts AirgapTransferOptions + remote string + contains []string + }{ + { + name: "native edgelet decompresses and loads", + opts: AirgapTransferOptions{DeploymentType: DeploymentTypeNative, Engine: EngineEdgelet}, + remote: "/tmp/iofogctl-airgap/host/router-linux_amd64.tar.gz", + contains: []string{ + "gunzip -c", + "edgelet image load -f", + }, + }, + { + name: "native docker uses docker load", + opts: AirgapTransferOptions{DeploymentType: DeploymentTypeNative, Engine: EngineDocker}, + remote: "/tmp/archive.tar.gz", + contains: []string{"sudo -S docker load -i /tmp/archive.tar.gz"}, + }, + { + name: "container podman uses podman load", + opts: AirgapTransferOptions{DeploymentType: DeploymentTypeContainer, Engine: EnginePodman}, + remote: "/tmp/archive.tar.gz", + contains: []string{"sudo -S podman load -i /tmp/archive.tar.gz"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ImageLoadCommand(tt.opts, tt.remote) + for _, want := range tt.contains { + if !containsSubstring(got, want) { + t.Fatalf("ImageLoadCommand() = %q, want substring %q", got, want) + } + } + }) + } +} + +func TestAirgapTransferOptionsFromConfig(t *testing.T) { + cfg := &rsc.AgentConfiguration{ + AgentConfiguration: client.AgentConfiguration{ + DeploymentType: strPtr("native"), + }, + } + opts, err := AirgapTransferOptionsFromConfig(cfg) + if err != nil { + t.Fatalf("AirgapTransferOptionsFromConfig: %v", err) + } + if opts.DeploymentType != DeploymentTypeNative { + t.Fatalf("deploymentType = %q, want native", opts.DeploymentType) + } + if opts.Engine != EngineEdgelet { + t.Fatalf("engine = %q, want edgelet", opts.Engine) + } + + cfg = &rsc.AgentConfiguration{ + AgentConfiguration: client.AgentConfiguration{ + DeploymentType: strPtr("container"), + ContainerEngine: strPtr("docker"), + }, + } + opts, err = AirgapTransferOptionsFromConfig(cfg) + if err != nil { + t.Fatalf("AirgapTransferOptionsFromConfig container: %v", err) + } + if opts.Engine != EngineDocker { + t.Fatalf("engine = %q, want docker", opts.Engine) + } +} + +func TestValidateAirgapRequirements(t *testing.T) { + nativeEdgelet := &rsc.AgentConfiguration{ + Arch: strPtr("amd64"), + AgentConfiguration: client.AgentConfiguration{ + DeploymentType: strPtr("native"), + ContainerEngine: strPtr("edgelet"), + }, + } + if err := ValidateAirgapRequirements(nativeEdgelet); err != nil { + t.Fatalf("native edgelet should be valid: %v", err) + } + + containerMissingEngine := &rsc.AgentConfiguration{ + Arch: strPtr("amd64"), + AgentConfiguration: client.AgentConfiguration{ + DeploymentType: strPtr("container"), + }, + } + if err := ValidateAirgapRequirements(containerMissingEngine); err == nil { + t.Fatal("expected container airgap to require docker or podman") + } + + nativeBadEngine := &rsc.AgentConfiguration{ + Arch: strPtr("amd64"), + AgentConfiguration: client.AgentConfiguration{ + DeploymentType: strPtr("native"), + ContainerEngine: strPtr("cri-o"), + }, + } + if err := ValidateAirgapRequirements(nativeBadEngine); err == nil { + t.Fatal("expected unsupported native engine to fail validation") + } +} + +func TestCollectAgentAirgapImages(t *testing.T) { + images := &RequiredImages{ + Agent: "ghcr.io/example/edgelet:1.0", + RouterAMD64: "ghcr.io/example/router:1.0", + NatsAMD64: "ghcr.io/example/nats:1.0", + DebuggerAMD64: "ghcr.io/example/debugger:1.0", + } + + nativeList, err := CollectAgentAirgapImages(images, PlatformAMD64, DeploymentTypeNative) + if err != nil { + t.Fatalf("CollectAgentAirgapImages native: %v", err) + } + for _, ref := range nativeList { + if ref == images.Agent { + t.Fatalf("native list should not include edgelet container image, got %v", nativeList) + } + } + if len(nativeList) != 3 { + t.Fatalf("native list len = %d, want 3 (%v)", len(nativeList), nativeList) + } + + containerList, err := CollectAgentAirgapImages(images, PlatformAMD64, DeploymentTypeContainer) + if err != nil { + t.Fatalf("CollectAgentAirgapImages container: %v", err) + } + if containerList[0] != images.Agent { + t.Fatalf("container list should include edgelet image first, got %v", containerList) + } +} + +func TestControllerAirgapLoadOptions(t *testing.T) { + cfg := &rsc.AgentConfiguration{ + AgentConfiguration: client.AgentConfiguration{ + ContainerEngine: strPtr("docker"), + }, + } + opts, err := ControllerAirgapLoadOptions(cfg) + if err != nil { + t.Fatalf("ControllerAirgapLoadOptions: %v", err) + } + if opts.Engine != EngineDocker || opts.DeploymentType != DeploymentTypeContainer { + t.Fatalf("unexpected opts: %+v", opts) + } + + cfg = &rsc.AgentConfiguration{ + AgentConfiguration: client.AgentConfiguration{ + ContainerEngine: strPtr("edgelet"), + }, + } + if _, err := ControllerAirgapLoadOptions(cfg); err == nil { + t.Fatal("expected controller load options to reject edgelet engine") + } +} + +func TestPlatformToOSArch(t *testing.T) { + osName, archName, err := PlatformToOSArch(PlatformARM64) + if err != nil { + t.Fatalf("PlatformToOSArch: %v", err) + } + if osName != "linux" || archName != "arm64" { + t.Fatalf("got %s/%s, want linux/arm64", osName, archName) + } +} + +func containsSubstring(s, sub string) bool { + return len(s) >= len(sub) && strings.Contains(s, sub) +} + +func TestResolveAgentDeploymentDefaultsNative(t *testing.T) { + cfg := &rsc.AgentConfiguration{} + if ResolveAgentDeployment(cfg, "") { + t.Fatal("expected native deployment") + } + if cfg.DeploymentType == nil || *cfg.DeploymentType != DeploymentTypeNative { + t.Fatalf("deploymentType = %v, want native", cfg.DeploymentType) + } + if cfg.ContainerEngine == nil || *cfg.ContainerEngine != string(EngineEdgelet) { + t.Fatalf("containerEngine = %v, want edgelet", cfg.ContainerEngine) + } +} + +func TestResolveAgentDeploymentContainerImage(t *testing.T) { + cfg := &rsc.AgentConfiguration{} + if !ResolveAgentDeployment(cfg, "ghcr.io/example/edgelet:1.0") { + t.Fatal("expected container deployment when image is set") + } + if *cfg.DeploymentType != DeploymentTypeContainer { + t.Fatalf("deploymentType = %q, want container", *cfg.DeploymentType) + } +} + +func TestEdgeletInstallConfigDefaults(t *testing.T) { + cfg := EdgeletInstallConfig("linux", &rsc.AgentConfiguration{}, rsc.Package{}) + if cfg.DeploymentType != DeploymentTypeNative { + t.Fatalf("deploymentType = %q, want native", cfg.DeploymentType) + } + if cfg.ContainerEngine != string(EngineEdgelet) { + t.Fatalf("containerEngine = %q, want edgelet", cfg.ContainerEngine) + } + if cfg.Runtime == nil { + t.Fatal("expected runtime spec") + } +} diff --git a/internal/deploy/airgap/images.go b/internal/deploy/airgap/images.go index 3621bcafa..f7ffc3176 100644 --- a/internal/deploy/airgap/images.go +++ b/internal/deploy/airgap/images.go @@ -12,12 +12,20 @@ import ( // RequiredImages represents all images needed for airgap deployment type RequiredImages struct { - Controller string - Agent string - RouterX86 string - RouterARM string - Nats string - Debugger string + Controller string + Agent string + RouterAMD64 string + RouterARM64 string + RouterRISCV64 string + RouterARM string + NatsAMD64 string + NatsARM64 string + NatsRISCV64 string + NatsARM string + DebuggerAMD64 string + DebuggerARM64 string + DebuggerRISCV64 string + DebuggerARM string } // getCatalogItemByName tries the given name, then fallbackNames if the first lookup fails (e.g. casing). @@ -41,9 +49,13 @@ func applyRouterImagesFromCatalog(images *RequiredImages, item *client.CatalogIt return } for _, img := range item.Images { - switch client.AgentTypeIDAgentTypeDict[img.AgentTypeID] { - case "x86": - images.RouterX86 = img.ContainerImage + switch client.ArchIDToName[img.ArchID] { + case "amd64": + images.RouterAMD64 = img.ContainerImage + case "arm64": + images.RouterARM64 = img.ContainerImage + case "riscv64": + images.RouterRISCV64 = img.ContainerImage case "arm": images.RouterARM = img.ContainerImage } @@ -56,9 +68,15 @@ func applyDebuggerImageFromCatalog(images *RequiredImages, item *client.CatalogI return } for _, img := range item.Images { - if client.AgentTypeIDAgentTypeDict[img.AgentTypeID] == "x86" || client.AgentTypeIDAgentTypeDict[img.AgentTypeID] == "arm" { - images.Debugger = img.ContainerImage - return + switch client.ArchIDToName[img.ArchID] { + case "amd64": + images.DebuggerAMD64 = img.ContainerImage + case "arm64": + images.DebuggerARM64 = img.ContainerImage + case "riscv64": + images.DebuggerRISCV64 = img.ContainerImage + case "arm": + images.DebuggerARM = img.ContainerImage } } } @@ -68,7 +86,18 @@ func applyNatsImageFromCatalog(images *RequiredImages, item *client.CatalogItemI if item == nil || len(item.Images) == 0 { return } - images.Nats = item.Images[0].ContainerImage + for _, img := range item.Images { + switch client.ArchIDToName[img.ArchID] { + case "amd64": + images.NatsAMD64 = img.ContainerImage + case "arm64": + images.NatsARM64 = img.ContainerImage + case "riscv64": + images.NatsRISCV64 = img.ContainerImage + case "arm": + images.NatsARM = img.ContainerImage + } + } } // applyYAMLFallbackForController fills any empty router/nats/debugger from controlPlane.SystemMicroservices; util is last fallback. @@ -77,67 +106,120 @@ func applyYAMLAndUtilFallbackForController(images *RequiredImages, controlPlane return } sm := &controlPlane.SystemMicroservices - if images.RouterX86 == "" { - if sm.Router.X86 != "" { - images.RouterX86 = sm.Router.X86 + if images.RouterAMD64 == "" { + if sm.Router.AMD64 != "" { + images.RouterAMD64 = sm.Router.AMD64 + } else { + images.RouterAMD64 = util.GetRouterImage() + } + } + if images.RouterARM64 == "" { + if sm.Router.ARM != "" { + images.RouterARM64 = sm.Router.ARM64 } else { - images.RouterX86 = util.GetRouterImage() + images.RouterARM64 = util.GetRouterImage() + } + } + if images.RouterRISCV64 == "" { + if sm.Router.RISCV64 != "" { + images.RouterRISCV64 = sm.Router.RISCV64 + } else { + images.RouterRISCV64 = util.GetRouterImage() } } if images.RouterARM == "" { if sm.Router.ARM != "" { images.RouterARM = sm.Router.ARM } else { - images.RouterARM = util.GetRouterARMImage() + images.RouterARM = util.GetRouterImage() + } + } + if images.NatsAMD64 == "" { + if sm.Nats.AMD64 != "" { + images.NatsAMD64 = sm.Nats.AMD64 + } else if sm.Nats.ARM64 != "" { + images.NatsARM64 = sm.Nats.ARM64 + } else { + images.NatsAMD64 = util.GetNatsImage() + } + } + if images.NatsARM64 == "" { + if sm.Nats.ARM64 != "" { + images.NatsARM64 = sm.Nats.ARM64 + } else { + images.NatsARM64 = util.GetNatsImage() + } + } + if images.NatsRISCV64 == "" { + if sm.Nats.RISCV64 != "" { + images.NatsRISCV64 = sm.Nats.RISCV64 + } else { + images.NatsRISCV64 = util.GetNatsImage() } } - if images.Nats == "" { - if sm.Nats.X86 != "" { - images.Nats = sm.Nats.X86 - } else if sm.Nats.ARM != "" { - images.Nats = sm.Nats.ARM + if images.NatsARM == "" { + if sm.Nats.ARM != "" { + images.NatsARM = sm.Nats.ARM } else { - images.Nats = util.GetNatsImage() + images.NatsARM = util.GetNatsImage() } } - if images.Debugger == "" { - images.Debugger = util.GetDebuggerImage() + if images.DebuggerAMD64 == "" { + images.DebuggerAMD64 = util.GetDebuggerImage() + } + if images.DebuggerARM64 == "" { + images.DebuggerARM64 = util.GetDebuggerImage() + } + if images.DebuggerRISCV64 == "" { + images.DebuggerRISCV64 = util.GetDebuggerImage() + } + if images.DebuggerARM == "" { + images.DebuggerARM = util.GetDebuggerImage() } } // applyYAMLAndUtilFallbackForAgent fills any empty router/nats/debugger from controlPlane (if non-nil) then util. func applyYAMLAndUtilFallbackForAgent(images *RequiredImages, controlPlane *rsc.RemoteControlPlane) { - if images.RouterX86 == "" { - if controlPlane != nil && controlPlane.SystemMicroservices.Router.X86 != "" { - images.RouterX86 = controlPlane.SystemMicroservices.Router.X86 + if images.RouterAMD64 == "" { + if controlPlane != nil && controlPlane.SystemMicroservices.Router.AMD64 != "" { + images.RouterAMD64 = controlPlane.SystemMicroservices.Router.AMD64 } else { - images.RouterX86 = util.GetRouterImage() + images.RouterAMD64 = util.GetRouterImage() } } - if images.RouterARM == "" { - if controlPlane != nil && controlPlane.SystemMicroservices.Router.ARM != "" { - images.RouterARM = controlPlane.SystemMicroservices.Router.ARM + if images.RouterARM64 == "" { + if controlPlane != nil && controlPlane.SystemMicroservices.Router.ARM64 != "" { + images.RouterARM64 = controlPlane.SystemMicroservices.Router.ARM64 } else { - images.RouterARM = util.GetRouterARMImage() + images.RouterARM64 = util.GetRouterImage() } } - if images.Nats == "" { + if images.NatsAMD64 == "" { if controlPlane != nil { - if controlPlane.SystemMicroservices.Nats.X86 != "" { - images.Nats = controlPlane.SystemMicroservices.Nats.X86 + if controlPlane.SystemMicroservices.Nats.AMD64 != "" { + images.NatsAMD64 = controlPlane.SystemMicroservices.Nats.AMD64 } else if controlPlane.SystemMicroservices.Nats.ARM != "" { - images.Nats = controlPlane.SystemMicroservices.Nats.ARM + images.NatsARM64 = controlPlane.SystemMicroservices.Nats.ARM64 } - if images.Nats == "" { - images.Nats = util.GetNatsImage() + if images.NatsAMD64 == "" { + images.NatsAMD64 = util.GetNatsImage() } } else { - images.Nats = util.GetNatsImage() + images.NatsAMD64 = util.GetNatsImage() } } - if images.Debugger == "" { + if images.DebuggerAMD64 == "" { // RemoteSystemMicroservices has no Debugger field; use util as fallback - images.Debugger = util.GetDebuggerImage() + images.DebuggerAMD64 = util.GetDebuggerImage() + } + if images.DebuggerARM64 == "" { + images.DebuggerARM64 = util.GetDebuggerImage() + } + if images.DebuggerRISCV64 == "" { + images.DebuggerRISCV64 = util.GetDebuggerImage() + } + if images.DebuggerARM == "" { + images.DebuggerARM = util.GetDebuggerImage() } } @@ -148,8 +230,8 @@ func CollectControllerImages(namespace string, controlPlane *rsc.RemoteControlPl images := &RequiredImages{} // Controller image - if controlPlane.Package.Container.Image != "" { - images.Controller = controlPlane.Package.Container.Image + if controlPlane.Controller.Package != nil && controlPlane.Controller.Package.Image != "" { + images.Controller = controlPlane.Controller.Package.Image } else { images.Controller = util.GetControllerImage() } @@ -231,8 +313,12 @@ func CollectAgentImages(namespace string, agent *rsc.RemoteAgent, controlPlane * func GetImageForPlatform(images *RequiredImages, platform string) (string, error) { switch platform { case PlatformAMD64: - return images.RouterX86, nil + return images.RouterAMD64, nil case PlatformARM64: + return images.RouterARM64, nil + case PlatformRISCV64: + return images.RouterRISCV64, nil + case PlatformARM: return images.RouterARM, nil default: return "", util.NewInputError(fmt.Sprintf("unsupported platform %s", platform)) diff --git a/internal/deploy/airgap/transfer.go b/internal/deploy/airgap/transfer.go index 1c1ba69d9..5f79d63af 100644 --- a/internal/deploy/airgap/transfer.go +++ b/internal/deploy/airgap/transfer.go @@ -12,14 +12,14 @@ import ( "strings" "time" - "github.com/containers/image/v5/copy" - "github.com/containers/image/v5/signature" - "github.com/containers/image/v5/transports/alltransports" - "github.com/containers/image/v5/types" "github.com/eclipse-iofog/iofogctl/internal/config" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" "github.com/eclipse-iofog/iofogctl/pkg/util" "github.com/opencontainers/go-digest" + "go.podman.io/image/v5/copy" + "go.podman.io/image/v5/signature" + "go.podman.io/image/v5/transports/alltransports" + "go.podman.io/image/v5/types" ) const ( @@ -41,12 +41,12 @@ type transferPlan struct { host string ssh *rsc.SSH platform string - engine ContainerEngine + opts AirgapTransferOptions images []string // List of image references to transfer } // TransferAirgapImages transfers required images to a remote host for airgap deployment -func TransferAirgapImages(ctx context.Context, namespace string, host string, ssh *rsc.SSH, platform string, engine ContainerEngine, images []string) error { +func TransferAirgapImages(ctx context.Context, namespace string, host string, ssh *rsc.SSH, platform string, opts AirgapTransferOptions, images []string) error { // Validate inputs if host == "" { return util.NewInputError("host is required for airgap image transfer") @@ -65,7 +65,7 @@ func TransferAirgapImages(ctx context.Context, namespace string, host string, ss host: host, ssh: ssh, platform: platform, - engine: engine, + opts: opts, images: images, } @@ -110,7 +110,7 @@ func ensureArtifact(ctx context.Context, platform, imageRef string, namespace st } cacheDir := config.GetAirgapImageCacheDir(namespace, imageRef, platform) - if err := os.MkdirAll(cacheDir, 0o755); err != nil { + if err := os.MkdirAll(cacheDir, util.DirPerm); err != nil { return nil, err } archivePath := filepath.Join(cacheDir, archiveFilename) @@ -186,7 +186,7 @@ func buildSystemContext(platform string, auth *rsc.OfflineImageAuth) (*types.Sys // pullCompressedImage pulls an image and compresses it to a tar.gz file func pullCompressedImage(ctx context.Context, imageRef, archivePath string, sysCtx *types.SystemContext, label string) (digestValue string, checksum string, size int64, err error) { destDir := filepath.Dir(archivePath) - if err := os.MkdirAll(destDir, 0o755); err != nil { + if err := os.MkdirAll(destDir, util.DirPerm); err != nil { return "", "", 0, err } rawPath := archivePath + ".raw" @@ -215,7 +215,7 @@ func pullCompressedImage(ctx context.Context, imageRef, archivePath string, sysC if err != nil { return "", "", 0, err } - defer policyCtx.Destroy() + defer func() { _ = policyCtx.Destroy() }() util.PrintInfo(label) manifestBytes, err := copy.Image(ctx, policyCtx, destRef, srcRef, ©.Options{ @@ -261,21 +261,21 @@ func insecurePolicyContext() (*signature.PolicyContext, error) { // compressToGzip compresses a file to gzip format func compressToGzip(src, dst string) error { - source, err := os.Open(src) + source, err := util.OpenValidatedFile(src) if err != nil { return err } - defer source.Close() + defer util.IgnoreClose(source) if err := os.RemoveAll(dst); err != nil && !os.IsNotExist(err) { return err } - destFile, err := os.Create(dst) + destFile, err := util.CreateUserFile(dst, util.FilePerm) if err != nil { return err } - defer destFile.Close() + defer util.IgnoreClose(destFile) gzipWriter := gzip.NewWriter(destFile) defer gzipWriter.Close() @@ -289,11 +289,11 @@ func compressToGzip(src, dst string) error { // calculateFileChecksum calculates SHA256 checksum and size of a file func calculateFileChecksum(path string) (string, int64, error) { - file, err := os.Open(path) + file, err := util.OpenValidatedFile(path) if err != nil { return "", 0, err } - defer file.Close() + defer util.IgnoreClose(file) hasher := sha256.New() size, err := io.Copy(hasher, file) @@ -322,11 +322,11 @@ func transferAndLoadImage(plan transferPlan, artifact *imageArtifact) error { } // Open and transfer file - file, err := os.Open(artifact.path) + file, err := util.OpenValidatedFile(artifact.path) if err != nil { return err } - defer file.Close() + defer util.IgnoreClose(file) info, err := file.Stat() if err != nil { return err @@ -343,13 +343,13 @@ func transferAndLoadImage(plan transferPlan, artifact *imageArtifact) error { } remotePath := util.JoinAgentPath(hostDir, filename) - // Load image using container engine - loadCmd := fmt.Sprintf("sudo -S %s load -i %s", plan.engine.Command(), remotePath) + // Load image using deployment/engine matrix + loadCmd := ImageLoadCommand(plan.opts, remotePath) if _, err := ssh.Run(loadCmd); err != nil { return fmt.Errorf("failed to load image: %w", err) } - // Clean up remote file + // Clean up remote archive (decompressed tar removed by edgelet load command) if _, err := ssh.Run("sudo rm -f " + remotePath); err != nil { util.PrintNotify(fmt.Sprintf("Warning: Failed to remove remote file %s: %v", remotePath, err)) } diff --git a/internal/deploy/application/factory.go b/internal/deploy/application/factory.go index d7cb11240..ab18bc4df 100644 --- a/internal/deploy/application/factory.go +++ b/internal/deploy/application/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployapplication import ( diff --git a/internal/deploy/applicationtemplate/factory.go b/internal/deploy/applicationtemplate/factory.go index efa8a0f77..4998da458 100644 --- a/internal/deploy/applicationtemplate/factory.go +++ b/internal/deploy/applicationtemplate/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployapplicationtemplate import ( diff --git a/internal/deploy/catalogitem/catalog_item.go b/internal/deploy/catalogitem/catalog_item.go index 7a8f04f05..f0d5060ab 100644 --- a/internal/deploy/catalogitem/catalog_item.go +++ b/internal/deploy/catalogitem/catalog_item.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deploycatalogitem import ( @@ -42,7 +29,7 @@ func (exe *remoteExecutor) GetName() string { } func (exe *remoteExecutor) updateCatalogItem(clt *client.Client) (err error) { - currentItem, err := clt.GetCatalogItem(exe.catalogItem.ID) + currentItem, err := clt.GetCatalogItemByName(exe.catalogItem.Name) if err != nil { return err } @@ -65,17 +52,31 @@ func (exe *remoteExecutor) updateCatalogItem(clt *client.Client) (err error) { request.RegistryID = registryID } - if exe.catalogItem.X86 != "" { + if exe.catalogItem.AMD64 != "" { + request.Images = append(request.Images, client.CatalogImage{ + ContainerImage: exe.catalogItem.AMD64, + ArchID: client.ArchNameToID["amd64"], + }) + } + + if exe.catalogItem.ARM64 != "" { + request.Images = append(request.Images, client.CatalogImage{ + ContainerImage: exe.catalogItem.ARM64, + ArchID: client.ArchNameToID["arm64"], + }) + } + + if exe.catalogItem.RISCV64 != "" { request.Images = append(request.Images, client.CatalogImage{ - ContainerImage: exe.catalogItem.X86, - AgentTypeID: client.AgentTypeAgentTypeIDDict["x86"], + ContainerImage: exe.catalogItem.RISCV64, + ArchID: client.ArchNameToID["riscv64"], }) } if exe.catalogItem.ARM != "" { request.Images = append(request.Images, client.CatalogImage{ ContainerImage: exe.catalogItem.ARM, - AgentTypeID: client.AgentTypeAgentTypeIDDict["arm"], + ArchID: client.ArchNameToID["arm"], }) } @@ -90,8 +91,10 @@ func (exe *remoteExecutor) createCatalogItem(clt *client.Client) (err error) { if _, err = clt.CreateCatalogItem(&client.CatalogItemCreateRequest{ Name: exe.catalogItem.Name, Images: []client.CatalogImage{ - {ContainerImage: exe.catalogItem.X86, AgentTypeID: client.AgentTypeAgentTypeIDDict["x86"]}, - {ContainerImage: exe.catalogItem.ARM, AgentTypeID: client.AgentTypeAgentTypeIDDict["arm"]}, + {ContainerImage: exe.catalogItem.AMD64, ArchID: client.ArchNameToID["amd64"]}, + {ContainerImage: exe.catalogItem.ARM64, ArchID: client.ArchNameToID["arm64"]}, + {ContainerImage: exe.catalogItem.RISCV64, ArchID: client.ArchNameToID["riscv64"]}, + {ContainerImage: exe.catalogItem.ARM, ArchID: client.ArchNameToID["arm"]}, }, RegistryID: client.RegistryTypeRegistryTypeIDDict[exe.catalogItem.Registry], Description: exe.catalogItem.Description, @@ -156,7 +159,7 @@ func validate(opt *apps.CatalogItem) error { return err } - if opt.ARM == "" && opt.X86 == "" { + if opt.AMD64 == "" && opt.ARM64 == "" && opt.RISCV64 == "" && opt.ARM == "" { return util.NewInputError("At least one image must be specified") } diff --git a/internal/deploy/certificate/factory.go b/internal/deploy/certificate/factory.go index 2190c95b8..b249f0d3d 100644 --- a/internal/deploy/certificate/factory.go +++ b/internal/deploy/certificate/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deploycertificate import ( diff --git a/internal/deploy/configmap/factory.go b/internal/deploy/configmap/factory.go index 7ab0deabb..3efa019c9 100644 --- a/internal/deploy/configmap/factory.go +++ b/internal/deploy/configmap/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployconfigmap import ( diff --git a/internal/deploy/controller/local/local.go b/internal/deploy/controller/local/local.go index 23d731100..47969aac1 100644 --- a/internal/deploy/controller/local/local.go +++ b/internal/deploy/controller/local/local.go @@ -1,38 +1,16 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deploylocalcontroller import ( - "fmt" - "regexp" - "github.com/eclipse-iofog/iofogctl/internal/config" "github.com/eclipse-iofog/iofogctl/internal/execute" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" "github.com/eclipse-iofog/iofogctl/pkg/util" - - "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" ) type localExecutor struct { - namespace string - ctrl *rsc.LocalController - ctrlPlane *rsc.LocalControlPlane - client *install.LocalContainer - localControllerConfig *install.LocalContainerConfig - containersNames []string - iofogUser rsc.IofogUser + namespace string + ctrl *rsc.LocalController + ctrlPlane *rsc.LocalControlPlane } type Options struct { @@ -51,20 +29,14 @@ func NewExecutor(opt Options) (exe execute.Executor, err error) { controller.Name = opt.Name } - // Validate if err = Validate(&controller); err != nil { return } - // Get the Control Plane ns, err := config.GetNamespace(opt.Namespace) if err != nil { return nil, err } - // controlPlane, err := ns.GetControlPlane() - // if err != nil { - // return - // } baseControlPlane, err := ns.GetControlPlane() if err != nil { @@ -72,7 +44,7 @@ func NewExecutor(opt Options) (exe execute.Executor, err error) { } controlPlane, ok := baseControlPlane.(*rsc.LocalControlPlane) if !ok { - err = util.NewError("Could not convert Control Plane to Remote Control Plane") + err = util.NewError("Could not convert Control Plane to Local Control Plane") return } @@ -87,93 +59,12 @@ func NewExecutorWithoutParsing(namespace string, controlPlane *rsc.LocalControlP if err := util.IsLowerAlphanumeric("Controller", controller.GetName()); err != nil { return nil, err } - cli, err := install.NewLocalContainerClient() - if err != nil { - return nil, err - } - - // Instantiate executor - return newExecutor(namespace, controlPlane, controller, cli), nil -} -// TODO: Rewrite this pkg, don't need ctrl coming in here -func newExecutor(namespace string, controlPlane *rsc.LocalControlPlane, ctrl *rsc.LocalController, client *install.LocalContainer) *localExecutor { return &localExecutor{ namespace: namespace, - ctrl: ctrl, - client: client, - localControllerConfig: install.NewLocalControllerConfig(ctrl.Container.Image, install.Credentials{ - User: ctrl.Container.Credentials.User, - Password: ctrl.Container.Credentials.Password, - }, install.Auth{ - URL: controlPlane.Auth.URL, - Realm: controlPlane.Auth.Realm, - SSL: controlPlane.Auth.SSL, - RealmKey: controlPlane.Auth.RealmKey, - ControllerClient: controlPlane.Auth.ControllerClient, - ControllerSecret: controlPlane.Auth.ControllerSecret, - ViewerClient: controlPlane.Auth.ViewerClient, - }, install.Database{ - Provider: controlPlane.Database.Provider, - Host: controlPlane.Database.Host, - Port: controlPlane.Database.Port, - User: controlPlane.Database.User, - Password: controlPlane.Database.Password, - DatabaseName: controlPlane.Database.DatabaseName, - SSL: controlPlane.Database.SSL, - CA: controlPlane.Database.CA, - }, install.Events{ - AuditEnabled: controlPlane.Events.AuditEnabled, - RetentionDays: controlPlane.Events.RetentionDays, - CleanupInterval: controlPlane.Events.CleanupInterval, - CaptureIpAddress: controlPlane.Events.CaptureIpAddress, - }, localSystemImagesToInstall(controlPlane.SystemMicroservices, controlPlane.Nats)), - iofogUser: controlPlane.GetUser(), + ctrl: controller, ctrlPlane: controlPlane, - } -} - -func (exe *localExecutor) cleanContainers() { - for _, name := range exe.containersNames { - if errClean := exe.client.CleanContainer(name); errClean != nil { - util.PrintNotify(fmt.Sprintf("Could not clean Controller container: %v", errClean)) - } - } -} - -func (exe *localExecutor) deployContainers() error { - controllerContainerConfig := exe.localControllerConfig - controllerContainerName := controllerContainerConfig.ContainerName - - // Deploy controller image - util.SpinStart("Deploying Controller container") - - // If container already exists, clean it - if _, err := exe.client.GetContainerByName(controllerContainerName); err == nil { - if err := exe.client.CleanContainer(controllerContainerName); err != nil { - return err - } - } - - _, err := exe.client.DeployContainer(controllerContainerConfig) - if err != nil { - return err - } - - exe.containersNames = append(exe.containersNames, controllerContainerName) - // Wait for public API - util.SpinStart("Waiting for Controller API") - if err := exe.client.WaitForCommand( - install.GetLocalContainerName("controller", false), - regexp.MustCompile("\"status\":[ |\t]*\"online\""), - "iofog-controller", - "controller", - "status", - ); err != nil { - return err - } - - return nil + }, nil } func (exe *localExecutor) GetName() string { @@ -181,36 +72,7 @@ func (exe *localExecutor) GetName() string { } func (exe *localExecutor) Execute() error { - // Deploy Controller images - if err := exe.deployContainers(); err != nil { - exe.cleanContainers() - return err - } - - // Update controller (its a pointer, this is returned to caller) - controllerContainerConfig := exe.localControllerConfig - // Local controllers typically use HTTP by default - endpoint, err := util.GetControllerEndpoint(fmt.Sprintf("%s:%s", controllerContainerConfig.Host, controllerContainerConfig.Ports[0].Host), false) - if err != nil { - return err - } - - exe.ctrl.Endpoint = endpoint - exe.ctrl.Created = util.NowUTC() - return exe.ctrlPlane.UpdateController(exe.ctrl) -} - -func localSystemImagesToInstall(s *rsc.LocalSystemMicroservices, nats *rsc.NatsEnabledConfig) *install.LocalSystemImages { - // Always pass a non-nil struct so local controller gets default NATS_IMAGE_1/2 and NATS_ENABLED when not overridden - out := &install.LocalSystemImages{} - if s != nil { - out.Router = s.Router - out.Nats = s.Nats - } - if nats != nil && nats.Enabled != nil { - out.NatsEnabled = nats.Enabled - } - return out + return util.NewInputError("LocalController add-on deploy via edgelet is not implemented yet") } func Validate(ctrl rsc.Controller) error { diff --git a/internal/deploy/controller/local/local_test.go b/internal/deploy/controller/local/local_test.go new file mode 100644 index 000000000..1e0e3e71e --- /dev/null +++ b/internal/deploy/controller/local/local_test.go @@ -0,0 +1,64 @@ +package deploylocalcontroller + +import ( + "testing" + + "github.com/eclipse-iofog/iofogctl/internal/config" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestNewExecutorRequiresLocalControlPlaneInNamespace(t *testing.T) { + config.Init(t.TempDir()) + + _, err := NewExecutor(Options{ + Namespace: "default", + Yaml: []byte("container:\n image: ghcr.io/example/controller:1.0\n"), + Name: "iofog", + }) + require.Error(t, err) +} + +func TestNewExecutorAcceptsExistingLocalControlPlane(t *testing.T) { + config.Init(t.TempDir()) + + ns, err := config.GetNamespace("default") + require.NoError(t, err) + + arch := "amd64" + cp := &rsc.LocalControlPlane{ + IofogUser: rsc.IofogUser{Email: "user@domain.com"}, + Auth: rsc.Auth{ + Mode: "embedded", + Bootstrap: &rsc.AuthBootstrap{ + Username: "admin", + Password: "LocalTest12!", + }, + }, + SystemAgent: &rsc.SystemAgentConfig{ + AgentConfiguration: &rsc.AgentConfiguration{Arch: &arch}, + }, + } + require.NoError(t, cp.UpdateController(&rsc.LocalController{ + Name: "iofog", + Endpoint: "http://localhost:51121", + })) + ns.SetControlPlane(cp) + require.NoError(t, config.Flush()) + + exe, err := NewExecutor(Options{ + Namespace: "default", + Yaml: []byte("container:\n image: ghcr.io/example/controller:1.0\n"), + Name: "iofog", + }) + require.NoError(t, err) + require.NotNil(t, exe) + require.Equal(t, "iofog", exe.GetName()) +} + +func TestValidateRejectsInvalidControllerName(t *testing.T) { + err := Validate(&rsc.LocalController{Name: "Invalid_Name"}) + var inputErr *util.InputError + require.ErrorAs(t, err, &inputErr) +} diff --git a/internal/deploy/controller/remote/remote.go b/internal/deploy/controller/remote/remote.go index cb9693ca1..37a34725a 100644 --- a/internal/deploy/controller/remote/remote.go +++ b/internal/deploy/controller/remote/remote.go @@ -1,23 +1,10 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployremotecontroller import ( "github.com/eclipse-iofog/iofogctl/internal/config" + deployremotecontrolplane "github.com/eclipse-iofog/iofogctl/internal/deploy/controlplane/remote" "github.com/eclipse-iofog/iofogctl/internal/execute" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -43,12 +30,10 @@ func NewExecutor(opt Options) (exe execute.Executor, err error) { controller.Name = opt.Name } - // Validate if err = Validate(&controller); err != nil { return } - // Get the Control Plane ns, err := config.GetNamespace(opt.Namespace) if err != nil { return nil, err @@ -67,15 +52,11 @@ func NewExecutor(opt Options) (exe execute.Executor, err error) { } func newExecutor(namespace string, controlPlane *rsc.RemoteControlPlane, controller *rsc.RemoteController) *remoteExecutor { - executor := &remoteExecutor{ + return &remoteExecutor{ namespace: namespace, controlPlane: controlPlane, controller: controller, } - - // Set default values - executor.setDefaultValues() - return executor } func (exe *remoteExecutor) GetName() string { @@ -96,180 +77,92 @@ func NewExecutorWithoutParsing(namespace string, controlPlane *rsc.RemoteControl return nil, err } - // Instantiate executor return newExecutor(namespace, controlPlane, controller), nil } func (exe *remoteExecutor) Execute() (err error) { - if err = exe.controller.ValidateSSH(); err != nil { - return + if err = exe.controlPlane.SupportsControllerAddOn(); err != nil { + return err } - if exe.controller.LogLevel == "" { - exe.controller.LogLevel = "info" + if err = exe.controlPlane.ValidateControllerAddOnDatabase(); err != nil { + return err } - // Instantiate deployer - controllerOptions := &install.ControllerOptions{ - Namespace: exe.namespace, - User: exe.controller.SSH.User, - Host: exe.controller.Host, - Port: exe.controller.SSH.Port, - PrivKeyFilename: exe.controller.SSH.KeyFile, - PidBaseDir: exe.controller.PidBaseDir, - EcnViewerPort: exe.controller.EcnViewerPort, - EcnViewerURL: exe.controller.EcnViewerURL, - LogLevel: exe.controller.LogLevel, - Version: exe.controlPlane.Package.Version, - Image: exe.controlPlane.Package.Container.Image, - SystemMicroservices: exe.controlPlane.SystemMicroservices, - NatsEnabled: natsEnabledFromSpec(exe.controlPlane.Nats), - Vault: vaultSpecToInstall(exe.controlPlane.Vault), + if err = exe.controlPlane.ValidateControllerAddOn(exe.controller); err != nil { + return err } - - // Add HTTPS configuration if present - if exe.controller.Https != nil { - controllerOptions.Https = &install.Https{ - Enabled: exe.controller.Https.Enabled, - CACert: exe.controller.Https.CACert, - TLSCert: exe.controller.Https.TLSCert, - TLSKey: exe.controller.Https.TLSKey, - } + if err = exe.controller.ValidateSSH(); err != nil { + return err } - // Add SiteCA configuration if present - if exe.controller.SiteCA != nil { - controllerOptions.SiteCA = &install.SiteCertificate{ - TLSCert: exe.controller.SiteCA.TLSCert, - TLSKey: exe.controller.SiteCA.TLSKey, - } - } + applySystemMicroserviceDefaults(exe.controlPlane) - // Add LocalCA configuration if present - if exe.controller.LocalCA != nil { - controllerOptions.LocalCA = &install.SiteCertificate{ - TLSCert: exe.controller.LocalCA.TLSCert, - TLSKey: exe.controller.LocalCA.TLSKey, - } + edgelet, err := deployremotecontrolplane.DeployHostEdgelet(exe.controlPlane, exe.controller, exe.namespace) + if err != nil { + return err } - // Set airgap flag from control plane - controllerOptions.Airgap = exe.controlPlane.Airgap + translateOpts := deployremotecontrolplane.TranslateOptions{ + Name: exe.namespace, + Namespace: exe.namespace, + } - deployer, err := install.NewController(controllerOptions) + registryID, err := deployremotecontrolplane.DeployPrivateEdgeletRegistry(exe.controlPlane, edgelet, translateOpts) if err != nil { return err } - // Set custom scripts if provided - if exe.controller.Scripts != nil { - if err := deployer.CustomizeProcedures( - exe.controller.Scripts.Directory, - &exe.controller.Scripts.ControllerProcedures); err != nil { - return err - } + if err := deployremotecontrolplane.DeployEdgeletControlPlane(exe.controlPlane, exe.controller, edgelet, translateOpts, registryID); err != nil { + return err } - // Set database configuration - if exe.controlPlane.Database.Host != "" { - db := exe.controlPlane.Database - deployer.SetControllerExternalDatabase(db.Host, db.User, db.Password, db.Provider, db.DatabaseName, db.Port, db.SSL, db.CA) + endpoint, err := deployremotecontrolplane.ResolveControllerHostEndpoint(exe.controlPlane, exe.controller) + if err != nil { + return err } + exe.controller.Endpoint = endpoint + exe.controller.Created = util.NowUTC() - if exe.controlPlane.Auth.URL != "" { - auth := exe.controlPlane.Auth - deployer.SetControllerAuth(auth.URL, auth.Realm, auth.SSL, auth.RealmKey, auth.ControllerClient, auth.ControllerSecret, auth.ViewerClient) + if err := deployremotecontrolplane.DeployNextSystemAgent(exe.namespace, exe.controlPlane, exe.controller); err != nil { + return err } - // Set events configuration if present - if exe.controlPlane.Events.AuditEnabled != nil { - auditEnabled := *exe.controlPlane.Events.AuditEnabled - captureIpAddress := false - if exe.controlPlane.Events.CaptureIpAddress != nil { - captureIpAddress = *exe.controlPlane.Events.CaptureIpAddress - } - deployer.SetControllerEvents( - auditEnabled, - exe.controlPlane.Events.RetentionDays, - exe.controlPlane.Events.CleanupInterval, - captureIpAddress, - ) + if err := exe.controlPlane.UpdateController(exe.controller); err != nil { + return err } - // Deploy Controller - if err = deployer.Install(); err != nil { - return - } - // Update controller - useHTTPS := false - if exe.controller.Https != nil && exe.controller.Https.Enabled != nil && *exe.controller.Https.Enabled { - useHTTPS = true - } - exe.controller.Endpoint, err = util.GetControllerEndpoint(exe.controller.Host, useHTTPS) + ns, err := config.GetNamespace(exe.namespace) if err != nil { return err } - return exe.controlPlane.UpdateController(exe.controller) + ns.SetControlPlane(exe.controlPlane) + return config.Flush() } -func (exe *remoteExecutor) setDefaultValues() { - if exe.controlPlane.SystemMicroservices.Router.X86 == "" { - exe.controlPlane.SystemMicroservices.Router.X86 = util.GetRouterImage() +func applySystemMicroserviceDefaults(cp *rsc.RemoteControlPlane) { + if cp.SystemMicroservices.Router.AMD64 == "" { + cp.SystemMicroservices.Router.AMD64 = util.GetRouterImage() } - if exe.controlPlane.SystemMicroservices.Router.ARM == "" { - exe.controlPlane.SystemMicroservices.Router.ARM = util.GetRouterARMImage() + if cp.SystemMicroservices.Router.ARM64 == "" { + cp.SystemMicroservices.Router.ARM64 = util.GetRouterImage() } - if exe.controlPlane.SystemMicroservices.Nats.X86 == "" { - exe.controlPlane.SystemMicroservices.Nats.X86 = util.GetNatsImage() - } - if exe.controlPlane.SystemMicroservices.Nats.ARM == "" { - exe.controlPlane.SystemMicroservices.Nats.ARM = util.GetNatsImage() - } -} - -func natsEnabledFromSpec(n *rsc.NatsEnabledConfig) *bool { - if n == nil || n.Enabled == nil { - return nil - } - return n.Enabled -} - -func vaultSpecToInstall(v *rsc.VaultSpec) *install.VaultConfig { - if v == nil { - return nil + if cp.SystemMicroservices.Router.RISCV64 == "" { + cp.SystemMicroservices.Router.RISCV64 = util.GetRouterImage() } - out := &install.VaultConfig{ - Enabled: v.Enabled, - Provider: v.Provider, - BasePath: v.BasePath, + if cp.SystemMicroservices.Router.ARM == "" { + cp.SystemMicroservices.Router.ARM = util.GetRouterImage() } - if v.Hashicorp != nil { - out.Hashicorp = &install.VaultHashicorpConfig{ - Address: v.Hashicorp.Address, - Token: v.Hashicorp.Token, - Mount: v.Hashicorp.Mount, - } + if cp.SystemMicroservices.Nats.AMD64 == "" { + cp.SystemMicroservices.Nats.AMD64 = util.GetNatsImage() } - if v.Aws != nil { - out.Aws = &install.VaultAwsConfig{ - Region: v.Aws.Region, - AccessKeyId: v.Aws.AccessKeyId, - AccessKey: v.Aws.AccessKey, - } + if cp.SystemMicroservices.Nats.ARM64 == "" { + cp.SystemMicroservices.Nats.ARM64 = util.GetNatsImage() } - if v.Azure != nil { - out.Azure = &install.VaultAzureConfig{ - URL: v.Azure.URL, - TenantId: v.Azure.TenantId, - ClientId: v.Azure.ClientId, - ClientSecret: v.Azure.ClientSecret, - } + if cp.SystemMicroservices.Nats.RISCV64 == "" { + cp.SystemMicroservices.Nats.RISCV64 = util.GetNatsImage() } - if v.Google != nil { - out.Google = &install.VaultGoogleConfig{ - ProjectId: v.Google.ProjectId, - Credentials: v.Google.Credentials, - } + if cp.SystemMicroservices.Nats.ARM == "" { + cp.SystemMicroservices.Nats.ARM = util.GetNatsImage() } - return out } func Validate(ctrl rsc.Controller) error { diff --git a/internal/deploy/controlplane/k8s/execute.go b/internal/deploy/controlplane/k8s/execute.go index f59a47339..581e72e29 100644 --- a/internal/deploy/controlplane/k8s/execute.go +++ b/internal/deploy/controlplane/k8s/execute.go @@ -1,25 +1,16 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployk8scontrolplane import ( + "context" "fmt" cpv3 "github.com/eclipse-iofog/iofog-operator/v3/apis/controlplanes/v3" + "github.com/eclipse-iofog/iofogctl/internal/auth" "github.com/eclipse-iofog/iofogctl/internal/config" "github.com/eclipse-iofog/iofogctl/internal/execute" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/internal/trust" + inputvalidate "github.com/eclipse-iofog/iofogctl/internal/validate" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -90,57 +81,34 @@ func (exe *kubernetesControlPlaneExecutor) executeInstall() (err error) { return } - // Configure deploy + // Configure operator deploy (CLI-only image fields) installer.SetOperatorImage(exe.controlPlane.Images.Operator) installer.SetPullSecret(exe.controlPlane.Images.PullSecret) - installer.SetRouterImage(exe.controlPlane.Images.Router) - installer.SetControllerImage(exe.controlPlane.Images.Controller) - installer.SetNatsImage(exe.controlPlane.Images.Nats) - installer.SetControllerService(exe.controlPlane.Services.Controller.Type, exe.controlPlane.Services.Controller.Address, exe.controlPlane.Services.Controller.Annotations, exe.controlPlane.Services.Controller.ExternalTrafficPolicy) - installer.SetRouterService(exe.controlPlane.Services.Router.Type, exe.controlPlane.Services.Router.Address, exe.controlPlane.Services.Router.Annotations, exe.controlPlane.Services.Router.ExternalTrafficPolicy) - installer.SetNatsService(exe.controlPlane.Services.Nats.Type, exe.controlPlane.Services.Nats.Address, exe.controlPlane.Services.Nats.Annotations, exe.controlPlane.Services.Nats.ExternalTrafficPolicy) - installer.SetNatsServerService(exe.controlPlane.Services.NatsServer.Type, exe.controlPlane.Services.NatsServer.Address, exe.controlPlane.Services.NatsServer.Annotations, exe.controlPlane.Services.NatsServer.ExternalTrafficPolicy) - installer.SetControllerIngress(exe.controlPlane.Ingresses.Controller.Annotations, exe.controlPlane.Ingresses.Controller.IngressClassName, exe.controlPlane.Ingresses.Controller.Host, exe.controlPlane.Ingresses.Controller.SecretName) - installer.SetRouterIngress(exe.controlPlane.Ingresses.Router.Address, exe.controlPlane.Ingresses.Router.MessagePort, exe.controlPlane.Ingresses.Router.InteriorPort, exe.controlPlane.Ingresses.Router.EdgePort) - installer.SetNatsIngress(exe.controlPlane.Ingresses.Nats.Address, exe.controlPlane.Ingresses.Nats.ServerPort, exe.controlPlane.Ingresses.Nats.ClusterPort, exe.controlPlane.Ingresses.Nats.LeafPort, exe.controlPlane.Ingresses.Nats.MqttPort, exe.controlPlane.Ingresses.Nats.HttpPort) - // installer.SetRouterConfig(exe.controlPlane.Router.HA) - - // Set isViewerDns based on EcnViewerURL presence - if exe.controlPlane.Controller.EcnViewerURL != "" { - viewerDns := true - installer.SetIsViewerDns(&viewerDns) - } - - replicas := int32(1) - if exe.controlPlane.Replicas.Controller != 0 { - replicas = exe.controlPlane.Replicas.Controller - } - replicasNats := exe.controlPlane.Replicas.Nats - natsSpec := natsSpecToCpv3(exe.controlPlane.Nats) - vaultSpec := vaultSpecToCpv3(exe.controlPlane.Vault) - // Create controller on cluster - // user := install.IofogUser(exe.controlPlane.IofogUser) - conf := install.K8SControllerConfig{ - // User: user, - Replicas: replicas, - ReplicasNats: replicasNats, - Auth: install.Auth(exe.controlPlane.Auth), - Database: install.Database(exe.controlPlane.Database), - Events: install.Events(exe.controlPlane.Events), - PidBaseDir: exe.controlPlane.Controller.PidBaseDir, - EcnViewerPort: exe.controlPlane.Controller.EcnViewerPort, - EcnViewerURL: exe.controlPlane.Controller.EcnViewerURL, - LogLevel: exe.controlPlane.Controller.LogLevel, - Https: exe.controlPlane.Controller.Https, - SecretName: exe.controlPlane.Controller.SecretName, - Nats: natsSpec, - Vault: vaultSpec, - } - endpoint, err := installer.CreateControlPlane(&conf) + + desired := TranslateToControlPlaneCR(exe.controlPlane, exe.namespace) + if ca := rsc.GetTrustCA(exe.controlPlane); ca != "" { + if err := trust.StoreCA(exe.namespace, ca); err != nil { + return err + } + } + + endpoint, err := installer.CreateControlPlane(desired) if err != nil { return } + if err := trust.WaitForControllerAPI(context.Background(), exe.namespace, endpoint); err != nil { + return err + } + + if err := auth.EnsureIofogUserEmbedded(context.Background(), exe.namespace, endpoint, auth.EmbeddedAuthSpec{ + Mode: exe.controlPlane.Auth.Mode, + Bootstrap: exe.controlPlane.Auth.Bootstrap, + User: &exe.controlPlane.IofogUser, + }); err != nil { + return err + } + // Create controller pods for config pods, err := installer.GetControllerPods() if err != nil { @@ -159,28 +127,88 @@ func (exe *kubernetesControlPlaneExecutor) executeInstall() (err error) { // Assign control plane endpoint exe.controlPlane.Endpoint = endpoint + if exe.controlPlane.Controller.PublicUrl == "" { + exe.controlPlane.Controller.PublicUrl = endpoint + } + if err := rsc.BackfillConsoleURL(exe.controlPlane); err != nil { + return err + } return err } -const clusterIP = "ClusterIP" +const ( + clusterIP = "ClusterIP" + authModeEmbedded = "embedded" + authModeExternal = "external" +) func validateControlPlaneUser(controlPlane *rsc.KubernetesControlPlane) error { user := controlPlane.GetUser() if user.Email == "" { return util.NewInputError("Control Plane Iofog User must contain non-empty value in email field") } + if rawPassword := user.GetRawPassword(); rawPassword != "" { + if err := inputvalidate.ValidatePasswordComplexity(rawPassword); err != nil { + return err + } + } return nil } func validateControlPlaneAuth(controlPlane *rsc.KubernetesControlPlane) error { auth := controlPlane.Auth - if auth.URL == "" || auth.Realm == "" || auth.SSL == "" || auth.RealmKey == "" || auth.ControllerClient == "" || auth.ControllerSecret == "" || auth.ViewerClient == "" { - return util.NewInputError("Control Plane Auth Config must contain non-empty values in all fields") + switch auth.Mode { + case authModeEmbedded: + return validateEmbeddedAuth(auth) + case authModeExternal: + return validateExternalAuth(auth) + case "": + return util.NewInputError("Control Plane auth.mode is required (embedded or external)") + default: + return util.NewInputError(fmt.Sprintf("Control Plane auth.mode %q is invalid (embedded or external)", auth.Mode)) + } +} + +func validateEmbeddedAuth(auth rsc.Auth) error { + if auth.Bootstrap == nil { + return util.NewInputError("Control Plane auth.bootstrap is required when auth.mode is embedded") + } + if auth.Bootstrap.Username == "" { + return util.NewInputError("Control Plane auth.bootstrap.username is required when auth.mode is embedded") + } + if auth.Bootstrap.Password == "" { + return util.NewInputError("Control Plane auth.bootstrap.password is required in YAML when auth.mode is embedded") + } + if err := inputvalidate.ValidatePasswordComplexity(auth.Bootstrap.Password); err != nil { + return err } return nil } +func validateExternalAuth(auth rsc.Auth) error { + if auth.IssuerUrl == "" { + return util.NewInputError("Control Plane auth.issuerUrl is required when auth.mode is external") + } + if auth.Client == nil || auth.Client.ID == "" { + return util.NewInputError("Control Plane auth.client.id is required when auth.mode is external") + } + if auth.Client.Secret == "" { + return util.NewInputError("Control Plane auth.client.secret is required when auth.mode is external") + } + return nil +} + +func natsEnabled(controlPlane *rsc.KubernetesControlPlane) bool { + if controlPlane.Nats == nil { + return true + } + if controlPlane.Nats.Enabled == nil { + return true + } + return *controlPlane.Nats.Enabled +} + func validateControlPlaneDatabase(controlPlane *rsc.KubernetesControlPlane) error { db := controlPlane.Database replicas := controlPlane.Replicas.Controller @@ -216,6 +244,9 @@ func validateRouterServiceAndIngress(controlPlane *rsc.KubernetesControlPlane) e } func validateNatsReplicas(controlPlane *rsc.KubernetesControlPlane) error { + if !natsEnabled(controlPlane) { + return nil + } if controlPlane.Replicas.Nats > 0 && controlPlane.Replicas.Nats < 2 { return util.NewInputError("When NATS is enabled, replicas.nats must be at least 2") } diff --git a/internal/deploy/controlplane/k8s/execute_test.go b/internal/deploy/controlplane/k8s/execute_test.go new file mode 100644 index 000000000..5ac86eaa9 --- /dev/null +++ b/internal/deploy/controlplane/k8s/execute_test.go @@ -0,0 +1,133 @@ +package deployk8scontrolplane + +import ( + "testing" + + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/stretchr/testify/require" +) + +func requireInputError(t *testing.T, err error) { + t.Helper() + var inputErr *util.InputError + require.ErrorAs(t, err, &inputErr) +} + +func validControlPlane() *rsc.KubernetesControlPlane { + return &rsc.KubernetesControlPlane{ + IofogUser: rsc.IofogUser{Email: "user@domain.com"}, + Auth: rsc.Auth{ + Mode: authModeEmbedded, + Bootstrap: &rsc.AuthBootstrap{ + Username: "admin", + Password: "LocalTest12!", + }, + }, + Services: rsc.Services{ + Controller: rsc.Service{Type: "LoadBalancer"}, + Router: rsc.Service{Type: "LoadBalancer"}, + }, + Replicas: rsc.Replicas{Controller: 1, Nats: 2}, + } +} + +func TestValidateEmbeddedAuthMissingBootstrap(t *testing.T) { + cp := validControlPlane() + cp.Auth.Bootstrap = nil + err := validate(cp) + requireInputError(t, err) +} + +func TestValidateEmbeddedAuthWeakBootstrapPassword(t *testing.T) { + cp := validControlPlane() + cp.Auth.Bootstrap.Password = "short" + err := validate(cp) + requireInputError(t, err) +} + +func TestValidateExternalAuthMissingIssuerUrl(t *testing.T) { + cp := validControlPlane() + cp.Auth = rsc.Auth{ + Mode: authModeExternal, + Client: &rsc.AuthClient{ + ID: "controller", + Secret: "secret-value", + }, + } + err := validate(cp) + requireInputError(t, err) +} + +func TestValidateExternalAuthMissingClientSecret(t *testing.T) { + cp := validControlPlane() + cp.Auth = rsc.Auth{ + Mode: authModeExternal, + IssuerUrl: "https://auth.example.com/realms/myrealm", + Client: &rsc.AuthClient{ + ID: "controller", + }, + } + err := validate(cp) + requireInputError(t, err) +} + +func TestValidateExternalAuthValid(t *testing.T) { + cp := validControlPlane() + cp.Auth = rsc.Auth{ + Mode: authModeExternal, + IssuerUrl: "https://auth.example.com/realms/myrealm", + Client: &rsc.AuthClient{ + ID: "controller", + Secret: "secret-value", + }, + } + require.NoError(t, validate(cp)) +} + +func TestValidateIofogUserWeakPassword(t *testing.T) { + cp := validControlPlane() + cp.IofogUser.Password = "weak" + err := validate(cp) + requireInputError(t, err) +} + +func TestValidateControllerClusterIPWithoutIngress(t *testing.T) { + cp := validControlPlane() + cp.Services.Controller.Type = clusterIP + err := validate(cp) + requireInputError(t, err) +} + +func TestValidateRouterClusterIPWithoutIngress(t *testing.T) { + cp := validControlPlane() + cp.Services.Router.Type = clusterIP + err := validate(cp) + requireInputError(t, err) +} + +func TestValidateNatsReplicasOneWhenEnabled(t *testing.T) { + cp := validControlPlane() + cp.Replicas.Nats = 1 + err := validate(cp) + requireInputError(t, err) +} + +func TestValidateNatsReplicasSkippedWhenDisabled(t *testing.T) { + cp := validControlPlane() + disabled := false + cp.Nats = &rsc.NatsSpec{Enabled: &disabled} + cp.Replicas.Nats = 1 + require.NoError(t, validate(cp)) +} + +func TestValidateDatabaseHARequiresExternalDB(t *testing.T) { + cp := validControlPlane() + cp.Replicas.Controller = 2 + err := validate(cp) + requireInputError(t, err) +} + +func TestValidateValidEmbeddedControlPlane(t *testing.T) { + require.NoError(t, validate(validControlPlane())) +} diff --git a/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml b/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml new file mode 100644 index 000000000..eb640ba13 --- /dev/null +++ b/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml @@ -0,0 +1,66 @@ +apiVersion: datasance.com/v3 +kind: ControlPlane +metadata: + name: pot + namespace: test-ns +spec: + replicas: + controller: 2 + nats: 2 + controller: + publicUrl: https://controller.example.com + trustProxy: true + consolePort: 8080 + consoleUrl: https://controller.example.com + logLevel: info + auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true + images: + controller: ghcr.io/datasance/controller:3.8.0-rc.5 + router: ghcr.io/datasance/router:3.8.0-rc.1 + nats: ghcr.io/datasance/nats:2.14.2-rc.2 + nats: + enabled: true + jetStream: + storageSize: "10Gi" + memoryStoreSize: "1Gi" + services: + controller: + type: ClusterIP + annotations: {} + router: + type: ClusterIP + annotations: {} + nats: + type: ClusterIP + annotations: {} + natsServer: + type: ClusterIP + annotations: {} + ingresses: + controller: + annotations: {} + ingressClassName: nginx + host: controller.example.com + secretName: controller-tls + router: + address: router.example.com + messagePort: 5671 + interiorPort: 55671 + edgePort: 45671 + nats: + address: nats.example.com + serverPort: 4222 + clusterPort: 6222 + leafPort: 7422 + mqttPort: 8883 + httpPort: 8222 diff --git a/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml b/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml new file mode 100644 index 000000000..44fd2fb00 --- /dev/null +++ b/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml @@ -0,0 +1,66 @@ +apiVersion: iofog.org/v3 +kind: ControlPlane +metadata: + name: iofog + namespace: test-ns +spec: + replicas: + controller: 2 + nats: 2 + controller: + publicUrl: https://controller.example.com + trustProxy: true + consolePort: 8080 + consoleUrl: https://controller.example.com + logLevel: info + auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true + images: + controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.5 + router: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + nats: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + nats: + enabled: true + jetStream: + storageSize: "10Gi" + memoryStoreSize: "1Gi" + services: + controller: + type: ClusterIP + annotations: {} + router: + type: ClusterIP + annotations: {} + nats: + type: ClusterIP + annotations: {} + natsServer: + type: ClusterIP + annotations: {} + ingresses: + controller: + annotations: {} + ingressClassName: nginx + host: controller.example.com + secretName: controller-tls + router: + address: router.example.com + messagePort: 5671 + interiorPort: 55671 + edgePort: 45671 + nats: + address: nats.example.com + serverPort: 4222 + clusterPort: 6222 + leafPort: 7422 + mqttPort: 8883 + httpPort: 8222 diff --git a/internal/deploy/controlplane/k8s/translate.go b/internal/deploy/controlplane/k8s/translate.go new file mode 100644 index 000000000..9f28d1a39 --- /dev/null +++ b/internal/deploy/controlplane/k8s/translate.go @@ -0,0 +1,153 @@ +package deployk8scontrolplane + +import ( + cpv3 "github.com/eclipse-iofog/iofog-operator/v3/apis/controlplanes/v3" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/pkg/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type translateOptions struct { + apiVersion string + crName string + controllerImage string + routerImage string + natsImage string +} + +func defaultTranslateOptions() translateOptions { + return translateOptions{ + apiVersion: util.GetCliApiVersion(), + crName: util.GetCliCpCrName(), + controllerImage: util.GetControllerImage(), + routerImage: util.GetRouterImage(), + natsImage: util.GetNatsImage(), + } +} + +// TranslateToControlPlaneCR maps CLI KubernetesControlPlane spec to an operator ControlPlane CR. +// CLI-only fields (iofogUser, config, ca, images.operator) are omitted from the result. +func TranslateToControlPlaneCR(cp *rsc.KubernetesControlPlane, namespace string) cpv3.ControlPlane { + return translateToControlPlaneCR(cp, namespace, defaultTranslateOptions()) +} + +func translateToControlPlaneCR(cp *rsc.KubernetesControlPlane, namespace string, opts translateOptions) cpv3.ControlPlane { + replicas := int32(1) + if cp.Replicas.Controller != 0 { + replicas = cp.Replicas.Controller + } + spec := cpv3.ControlPlaneSpec{ + Auth: rsc.AuthToCPV3(cp.Auth), + Database: databaseToCPV3(cp.Database), + Events: eventsToCPV3(cp.Events), + Controller: rsc.ControllerConfigToCPV3(cp.Controller), + Replicas: cpv3.Replicas{ + Controller: replicas, + }, + Images: imagesToCPV3(cp.Images, opts), + Services: servicesToCPV3(cp.Services), + Ingresses: ingressesToCPV3(cp.Ingresses), + Nats: natsSpecToCpv3(cp.Nats), + Vault: vaultSpecToCpv3(cp.Vault), + } + if cp.Replicas.Nats >= 2 { + spec.Replicas.Nats = cp.Replicas.Nats + } + return cpv3.ControlPlane{ + TypeMeta: metav1.TypeMeta{ + APIVersion: opts.apiVersion, + Kind: "ControlPlane", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: opts.crName, + Namespace: namespace, + }, + Spec: spec, + } +} + +func databaseToCPV3(db rsc.Database) cpv3.Database { + return cpv3.Database{ + Provider: db.Provider, + Host: db.Host, + Port: db.Port, + User: db.User, + Password: db.Password, + DatabaseName: db.DatabaseName, + SSL: db.SSL, + CA: db.CA, + } +} + +func eventsToCPV3(ev rsc.Events) cpv3.Events { + return cpv3.Events{ + AuditEnabled: ev.AuditEnabled, + RetentionDays: ev.RetentionDays, + CleanupInterval: ev.CleanupInterval, + CaptureIpAddress: ev.CaptureIpAddress, + } +} + +func imagesToCPV3(img rsc.KubeImages, opts translateOptions) cpv3.Images { + controller := img.Controller + if controller == "" { + controller = opts.controllerImage + } + router := img.Router + if router == "" { + router = opts.routerImage + } + nats := img.Nats + if nats == "" { + nats = opts.natsImage + } + return cpv3.Images{ + PullSecret: img.PullSecret, + Controller: controller, + Router: router, + Nats: nats, + } +} + +func serviceToCPV3(s rsc.Service) cpv3.Service { + return cpv3.Service{ + Type: s.Type, + Address: s.Address, + Annotations: s.Annotations, + ExternalTrafficPolicy: s.ExternalTrafficPolicy, + } +} + +func servicesToCPV3(s rsc.Services) cpv3.Services { + return cpv3.Services{ + Controller: serviceToCPV3(s.Controller), + Router: serviceToCPV3(s.Router), + Nats: serviceToCPV3(s.Nats), + NatsServer: serviceToCPV3(s.NatsServer), + } +} + +func ingressesToCPV3(in rsc.Ingresses) cpv3.Ingresses { + return cpv3.Ingresses{ + Controller: cpv3.ControllerIngress{ + Annotations: in.Controller.Annotations, + IngressClassName: in.Controller.IngressClassName, + Host: in.Controller.Host, + SecretName: in.Controller.SecretName, + }, + Router: cpv3.RouterIngress{ + Address: in.Router.Address, + MessagePort: in.Router.MessagePort, + InteriorPort: in.Router.InteriorPort, + EdgePort: in.Router.EdgePort, + }, + Nats: cpv3.NatsIngress{ + Address: in.Nats.Address, + ServerPort: in.Nats.ServerPort, + ClusterPort: in.Nats.ClusterPort, + LeafPort: in.Nats.LeafPort, + MqttPort: in.Nats.MqttPort, + HttpPort: in.Nats.HttpPort, + }, + } +} diff --git a/internal/deploy/controlplane/k8s/translate_test.go b/internal/deploy/controlplane/k8s/translate_test.go new file mode 100644 index 000000000..f94cdf285 --- /dev/null +++ b/internal/deploy/controlplane/k8s/translate_test.go @@ -0,0 +1,124 @@ +package deployk8scontrolplane + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + cpv3 "github.com/eclipse-iofog/iofog-operator/v3/apis/controlplanes/v3" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" +) + +const testNamespace = "test-ns" + +func resourceFixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "..", "..", "..", "resource", "testdata", "k8s", name) +} + +func crFixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "testdata", name) +} + +func loadResourceFixture(t *testing.T, name string) rsc.KubernetesControlPlane { + t.Helper() + raw, err := os.ReadFile(resourceFixturePath(name)) + require.NoError(t, err) + cp, err := rsc.UnmarshallKubernetesControlPlane(raw) + require.NoError(t, err) + return cp +} + +func loadExpectedCR(t *testing.T, name string) cpv3.ControlPlane { + t.Helper() + raw, err := os.ReadFile(crFixturePath(name)) + require.NoError(t, err) + var cp cpv3.ControlPlane + require.NoError(t, yaml.UnmarshalStrict(raw, &cp)) + return cp +} + +func stripCRSecrets(cp *cpv3.ControlPlane) { + if cp.Spec.Auth.Bootstrap != nil { + cp.Spec.Auth.Bootstrap.Password = "" + } +} + +func assertTranslatedCR(t *testing.T, got, want cpv3.ControlPlane) { + t.Helper() + stripCRSecrets(&got) + stripCRSecrets(&want) + require.Equal(t, want.APIVersion, got.APIVersion) + require.Equal(t, want.Kind, got.Kind) + require.Equal(t, want.Name, got.Name) + require.Equal(t, want.Namespace, got.Namespace) + require.Equal(t, want.Spec, got.Spec) +} + +func TestTranslateToControlPlaneCR_DatasanceGolden(t *testing.T) { + cp := loadResourceFixture(t, "controlplane-datasance.yaml") + got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ + apiVersion: "datasance.com/v3", + crName: "pot", + controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.5", + routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", + natsImage: "ghcr.io/datasance/nats:2.14.2-rc.2", + }) + want := loadExpectedCR(t, "cp-cr-datasance.yaml") + assertTranslatedCR(t, got, want) +} + +func TestTranslateToControlPlaneCR_IofogGolden(t *testing.T) { + cp := loadResourceFixture(t, "controlplane-iofog.yaml") + got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ + apiVersion: "iofog.org/v3", + crName: "iofog", + controllerImage: "ghcr.io/eclipse-iofog/controller:3.8.0-rc.5", + routerImage: "ghcr.io/eclipse-iofog/router:3.8.0-rc.1", + natsImage: "ghcr.io/eclipse-iofog/nats:2.14.2-rc.2", + }) + want := loadExpectedCR(t, "cp-cr-iofog.yaml") + assertTranslatedCR(t, got, want) +} + +func TestTranslateToControlPlaneCR_StripsOperatorImage(t *testing.T) { + cp := loadResourceFixture(t, "controlplane-datasance.yaml") + require.NotEmpty(t, cp.Images.Operator) + got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ + apiVersion: "datasance.com/v3", + crName: "pot", + controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.5", + routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", + natsImage: "ghcr.io/datasance/nats:2.14.2-rc.2", + }) + require.NotContains(t, got.Spec.Images.Controller, "operator") + require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.5", got.Spec.Images.Controller) +} + +func TestTranslateToControlPlaneCR_DefaultImagesWhenOmitted(t *testing.T) { + cp := loadResourceFixture(t, "controlplane-datasance.yaml") + cp.Images.Controller = "" + cp.Images.Router = "" + cp.Images.Nats = "" + got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ + apiVersion: "datasance.com/v3", + crName: "pot", + controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.5", + routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", + natsImage: "ghcr.io/datasance/nats:2.14.2-rc.2", + }) + require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.5", got.Spec.Images.Controller) + require.Equal(t, "ghcr.io/datasance/router:3.8.0-rc.1", got.Spec.Images.Router) + require.Equal(t, "ghcr.io/datasance/nats:2.14.2-rc.2", got.Spec.Images.Nats) +} + +func TestTranslateToControlPlaneCR_CRNameFromLdflagDefault(t *testing.T) { + cp := loadResourceFixture(t, "controlplane-iofog.yaml") + got := TranslateToControlPlaneCR(&cp, testNamespace) + require.Equal(t, "iofog", got.Name) + require.Equal(t, "iofog.org/v3", got.APIVersion) +} diff --git a/internal/deploy/controlplane/local/edgelet_host.go b/internal/deploy/controlplane/local/edgelet_host.go new file mode 100644 index 000000000..088d25045 --- /dev/null +++ b/internal/deploy/controlplane/local/edgelet_host.go @@ -0,0 +1,230 @@ +package deploylocalcontrolplane + +import ( + "fmt" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/iofog" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func resolveLocalControlPlaneEndpoint(cp *rsc.LocalControlPlane) string { + if cp.Endpoint != "" { + return cp.Endpoint + } + if cp.Controller.PublicUrl != "" { + return cp.Controller.PublicUrl + } + return fmt.Sprintf("http://localhost:%s", iofog.ControllerPortString) +} + +// BuildLocalEdgelet constructs a LocalEdgelet from LocalControlPlane systemAgent settings. +func BuildLocalEdgelet(cp *rsc.LocalControlPlane, name, agentUUID string) (*install.LocalEdgelet, error) { + sys := cp.SystemAgent + var cfg *rsc.AgentConfiguration + var pkg rsc.Package + var scripts *rsc.AgentScripts + if sys != nil { + cfg = sys.AgentConfiguration + pkg = sys.Package + scripts = sys.Scripts + } + + cfg = deployairgap.EnsureAgentConfig(cfg) + deployairgap.ResolveAgentDeployment(cfg, pkg.Container.Image) + + installCfg := deployairgap.EdgeletInstallConfig(deployairgap.LocalEdgeletHostOS(), cfg, pkg) + edgelet, err := install.NewLocalEdgelet(name, agentUUID, installCfg) + if err != nil { + return nil, err + } + + if scripts != nil { + procs := install.EdgeletProcedures{AgentProcedures: scripts.AgentProcedures} + if err := edgelet.CustomizeProcedures(scripts.Directory, &procs); err != nil { + return nil, err + } + } + if pkg.Container.Image != "" { + if err := edgelet.SetContainerImage(pkg.Container.Image); err != nil { + return nil, err + } + } else if pkg.Version != "" { + if err := edgelet.SetVersion(pkg.Version); err != nil { + return nil, err + } + } + return edgelet, nil +} + +// EdgeletHostTeardown runs host-level edgelet teardown commands. +type EdgeletHostTeardown interface { + Deprovision() error + DeleteControlPlane() error + Uninstall(removeData bool) error +} + +// BuildEdgeletForTeardown selects LocalEdgelet or RemoteEdgelet for control plane host teardown. +func BuildEdgeletForTeardown(cp *rsc.LocalControlPlane, namespace, name, agentUUID string) (EdgeletHostTeardown, error) { + if namespace != "" { + ns, err := config.GetNamespace(namespace) + if err == nil { + if baseAgent, err := ns.GetAgent(name); err == nil { + if remote, ok := baseAgent.(*rsc.RemoteAgent); ok && remote.ValidateSSH() == nil { + return buildRemoteEdgeletForTeardown(remote) + } + } + } + } + return BuildLocalEdgelet(cp, name, agentUUID) +} + +func buildRemoteEdgeletForTeardown(agent *rsc.RemoteAgent) (*install.RemoteEdgelet, error) { + cfg := deployairgap.EdgeletInstallConfig("linux", agent.Config, agent.Package) + edgelet, err := install.NewRemoteEdgelet( + agent.SSH.User, + agent.Host, + agent.SSH.Port, + agent.SSH.KeyFile, + agent.Name, + agent.UUID, + cfg, + ) + if err != nil { + return nil, err + } + if agent.Scripts != nil { + procs := install.EdgeletProcedures{AgentProcedures: agent.Scripts.AgentProcedures} + if err := edgelet.CustomizeProcedures(agent.Scripts.Directory, &procs); err != nil { + return nil, err + } + } + if agent.Package.Container.Image != "" { + if err := edgelet.SetContainerImage(agent.Package.Container.Image); err != nil { + return nil, err + } + } else if agent.Package.Version != "" { + if err := edgelet.SetVersion(agent.Package.Version); err != nil { + return nil, err + } + } + return edgelet, nil +} + +func installHostEdgelet(cp *rsc.LocalControlPlane, name string) (*install.LocalEdgelet, error) { + edgelet, err := BuildLocalEdgelet(cp, name, "") + if err != nil { + return nil, err + } + + util.SpinStart("Installing edgelet") + if err := edgelet.Bootstrap(); err != nil { + return nil, err + } + return edgelet, nil +} + +func deployPrivateEdgeletRegistry(cp *rsc.LocalControlPlane, edgelet *install.LocalEdgelet, opts TranslateOptions) (*int, error) { + if !NeedsPrivateEdgeletRegistry(cp) { + return nil, nil + } + + result, err := TranslateLocalControlPlane(cp, opts) + if err != nil { + return nil, err + } + if len(result.Registry) == 0 { + return nil, util.NewError("private registry translation produced no manifest") + } + + path, cleanup, err := edgelet.WriteDeployManifest(result.Registry, "edgelet-registry") + if err != nil { + return nil, err + } + defer cleanup() + + if err := edgelet.DeployFromFile(path); err != nil { + return nil, fmt.Errorf("edgelet registry deploy failed: %w", err) + } + + output, err := edgelet.RegistryList() + if err != nil { + return nil, fmt.Errorf("edgelet registry ls failed: %w", err) + } + + pkg := cp.Controller.Package + id, err := install.ParseEdgeletRegistryID(output, pkg.Registry, pkg.Username) + if err != nil { + return nil, err + } + return &id, nil +} + +func deployEdgeletControlPlane(cp *rsc.LocalControlPlane, edgelet *install.LocalEdgelet, opts TranslateOptions, registryID *int) error { + opts.RegistryID = ResolveEdgeletRegistryID(cp, registryID) + result, err := TranslateLocalControlPlane(cp, opts) + if err != nil { + return err + } + + path, cleanup, err := edgelet.WriteDeployManifest(result.ControlPlane, "edgelet-controlplane") + if err != nil { + return err + } + defer cleanup() + + if err := edgelet.DeployFromFile(path); err != nil { + return fmt.Errorf("edgelet control plane deploy failed: %w", err) + } + return nil +} + +func deployControllerRegistry(namespace string, cp *rsc.LocalControlPlane) error { + if !NeedsPrivateEdgeletRegistry(cp) { + return nil + } + pkg := cp.Controller.Package + if pkg == nil { + return nil + } + + clt, err := clientutil.NewControllerClient(namespace) + if err != nil { + return err + } + + url := pkg.Registry + username := pkg.Username + password := pkg.Password + email := pkg.Email + if email == "" { + email = "registry@local" + } + + createRequest := &client.RegistryCreateRequest{ + URL: url, + IsPublic: false, + Username: username, + Password: password, + Email: email, + } + if _, err = clt.CreateRegistry(createRequest); err != nil { + return fmt.Errorf("failed to register private registry with controller: %w", err) + } + return nil +} + +func persistLocalControllerStub(cp *rsc.LocalControlPlane, name, endpoint string) error { + controller := &rsc.LocalController{ + Name: name, + Endpoint: endpoint, + Created: util.NowUTC(), + } + cp.Endpoint = endpoint + return cp.UpdateController(controller) +} diff --git a/internal/deploy/controlplane/local/execute.go b/internal/deploy/controlplane/local/execute.go index ec4161f3b..9d071bef7 100644 --- a/internal/deploy/controlplane/local/execute.go +++ b/internal/deploy/controlplane/local/execute.go @@ -1,173 +1,82 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deploylocalcontrolplane import ( + "context" "fmt" - "net" - "net/url" - "strings" - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/auth" "github.com/eclipse-iofog/iofogctl/internal/config" - - // deployagentconfig "github.com/eclipse-iofog/iofogctl/internal/deploy/agentconfig" - deploylocalcontroller "github.com/eclipse-iofog/iofogctl/internal/deploy/controller/local" "github.com/eclipse-iofog/iofogctl/internal/execute" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - iutil "github.com/eclipse-iofog/iofogctl/internal/util" - - // clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/iofog" - "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/internal/trust" "github.com/eclipse-iofog/iofogctl/pkg/util" ) type Options struct { Namespace string Yaml []byte + FullYAML []byte Name string } + type localControlPlaneExecutor struct { - ctrlClient *client.Client - controllerExecutors []execute.Executor - controlPlane *rsc.LocalControlPlane - namespace string - name string + controlPlane *rsc.LocalControlPlane + namespace string + name string } -// func createDefaultRouter(clt *client.Client) (err error) { -// routerConfig := client.Router{ -// Host: "localhost", -// RouterConfig: client.RouterConfig{ -// RouterMode: iutil.MakeStrPtr("interior"), -// MessagingPort: iutil.MakeIntPtr(5671), -// EdgeRouterPort: iutil.MakeIntPtr(45671), -// InterRouterPort: iutil.MakeIntPtr(55671), -// }, -// } - -// return clt.PutDefaultRouter(routerConfig) -// } - -// prepareViewerURL prepares the viewer URL from endpoint using logic similar to view.go -func prepareViewerURL(endpoint string) (string, error) { - URL, err := url.Parse(endpoint) - if err != nil || URL.Host == "" { - URL, err = url.Parse("//" + endpoint) - if err != nil { - return "", fmt.Errorf("failed to parse endpoint: %v", err) - } - } - - if URL.Scheme == "" { - URL.Scheme = "http" - } +func (exe localControlPlaneExecutor) Execute() (err error) { + util.SpinStart(fmt.Sprintf("Deploying controlplane %s", exe.GetName())) - host := "" - if strings.Contains(URL.Host, ":") { - host, _, err = net.SplitHostPort(URL.Host) - if err != nil { - return "", fmt.Errorf("failed to split host and port: %v", err) + if ca := exe.controlPlane.GetTrustCA(); ca != "" { + if err := trust.StoreCA(exe.namespace, ca); err != nil { + return err } - } else { - host = URL.Host } - // Add port for localhost - if util.IsLocalHost(host) { - host = net.JoinHostPort(host, iofog.ControllerHostECNViewerPortString) + edgelet, err := installHostEdgelet(exe.controlPlane, exe.name) + if err != nil { + return err } - URL.Host = host - return URL.String(), nil -} - -// updateViewerClientRootURL updates the viewer client root URL in Keycloak if auth is configured -func updateViewerClientRootURL(controlPlane *rsc.LocalControlPlane, endpoint string) error { - // Check if auth is configured - if controlPlane.Auth.URL == "" || controlPlane.Auth.ViewerClient == "" { - // Auth not configured, skip update - return nil + translateOpts := TranslateOptions{ + Name: exe.name, + Namespace: exe.namespace, } - // Prepare viewer URL - viewerURL, err := prepareViewerURL(endpoint) + registryID, err := deployPrivateEdgeletRegistry(exe.controlPlane, edgelet, translateOpts) if err != nil { - return fmt.Errorf("failed to prepare viewer URL: %v", err) + return err } - // Update viewer client root URL - if err := iutil.UpdateECNViewerClientRootURL(controlPlane.Auth, viewerURL); err != nil { - return fmt.Errorf("failed to update viewer client root URL: %v", err) + if err := deployEdgeletControlPlane(exe.controlPlane, edgelet, translateOpts, registryID); err != nil { + return err } - return nil -} - -func (exe localControlPlaneExecutor) postDeploy() (err error) { - // Check controller is reachable - // clt, err := clientutil.NewControllerClient(exe.namespace) - // if err != nil { - // return err - // } - - // if err := createDefaultRouter(clt); err != nil { - // return err - // } - return nil -} + endpoint := resolveLocalControlPlaneEndpoint(exe.controlPlane) + if err := persistLocalControllerStub(exe.controlPlane, exe.name, endpoint); err != nil { + return err + } + if err := rsc.BackfillConsoleURL(exe.controlPlane); err != nil { + return err + } -func (exe localControlPlaneExecutor) Execute() (err error) { - util.SpinStart(fmt.Sprintf("Deploying controlplane %s", exe.GetName())) - if err := runExecutors(exe.controllerExecutors); err != nil { + if err := trust.WaitForControllerAPI(context.Background(), exe.namespace, endpoint); err != nil { return err } - // Make sure Controller API is ready - controller, err := exe.controlPlane.GetController("") - if err != nil { + if err := auth.EnsureIofogUserEmbedded(context.Background(), exe.namespace, endpoint, auth.EmbeddedAuthSpec{ + Mode: exe.controlPlane.Auth.Mode, + Bootstrap: exe.controlPlane.Auth.Bootstrap, + User: &exe.controlPlane.IofogUser, + }); err != nil { return err } - endpoint := controller.GetEndpoint() - if err := install.WaitForControllerAPI(endpoint); err != nil { + if err := deployControllerRegistry(exe.namespace, exe.controlPlane); err != nil { return err } - // // Create new user - // baseURL, err := util.GetBaseURL(endpoint) - // if err != nil { - // return err - // } - // exe.ctrlClient = client.New(client.Options{BaseURL: baseURL}) - // user := client.User(exe.controlPlane.GetUser()) - // user.Password = exe.controlPlane.GetUser().GetRawPassword() - // if err = exe.ctrlClient.CreateUser(user); err != nil { - // // If not error about account existing, fail - // if !strings.Contains(err.Error(), "already an account associated") { - // return err - // } - // // Try to log in - // if err := exe.ctrlClient.Login(client.LoginRequest{ - // Email: user.Email, - // Password: user.Password, - // }); err != nil { - // return err - // } - // } - // Update config ns, err := config.GetNamespace(exe.namespace) if err != nil { return err @@ -177,63 +86,36 @@ func (exe localControlPlaneExecutor) Execute() (err error) { return err } - // Update viewer client root URL if auth is configured - if err := updateViewerClientRootURL(exe.controlPlane, endpoint); err != nil { - // Log error but don't fail deployment - util.PrintInfo(fmt.Sprintf("Warning: Failed to update viewer client root URL: %v\n", err)) + if err := deployLocalSystemAgent(exe.namespace, exe.controlPlane, exe.name, edgelet); err != nil { + return err } - // Post deploy steps - return exe.postDeploy() + return nil } func (exe localControlPlaneExecutor) GetName() string { return exe.name } -func newControlPlaneExecutor(executors []execute.Executor, namespace, name string, controlPlane *rsc.LocalControlPlane) execute.Executor { - return localControlPlaneExecutor{ - controllerExecutors: executors, - namespace: namespace, - controlPlane: controlPlane, - name: name, - } -} - func NewExecutor(opt Options) (exe execute.Executor, err error) { - // Check the namespace exists _, err = config.GetNamespace(opt.Namespace) if err != nil { return } - // Read the input file + if len(opt.FullYAML) > 0 { + if err = rsc.ValidateLocalControlPlaneMetadata(opt.FullYAML); err != nil { + return + } + } controlPlane, err := rsc.UnmarshallLocalControlPlane(opt.Yaml) if err != nil { return } - // Create exe Controllers - controllers := controlPlane.GetControllers() - controllerExecutors := make([]execute.Executor, len(controllers)) - for idx := range controllers { - controller, ok := controllers[idx].(*rsc.LocalController) - if !ok { - return nil, util.NewError("Could not convert Controller to Local Controller") - } - exe, err := deploylocalcontroller.NewExecutorWithoutParsing(opt.Namespace, &controlPlane, controller) - if err != nil { - return nil, err - } - controllerExecutors[idx] = exe - } - - return newControlPlaneExecutor(controllerExecutors, opt.Namespace, opt.Name, &controlPlane), nil -} - -func runExecutors(executors []execute.Executor) error { - if errs, _ := execute.ForParallel(executors); len(errs) > 0 { - return execute.CoalesceErrors(errs) - } - return nil + return localControlPlaneExecutor{ + controlPlane: &controlPlane, + namespace: opt.Namespace, + name: opt.Name, + }, nil } diff --git a/internal/deploy/controlplane/local/manifest.go b/internal/deploy/controlplane/local/manifest.go new file mode 100644 index 000000000..c7640319c --- /dev/null +++ b/internal/deploy/controlplane/local/manifest.go @@ -0,0 +1,185 @@ +package deploylocalcontrolplane + +const ( + edgeletAPIVersion = "edgelet.iofog.org/v1" + edgeletControlPlaneKind = "ControlPlane" + edgeletRegistryKind = "Registry" + + // EdgeletRegistryOnline is docker.io (edgelet registry ls ID 1) for online pulls. + EdgeletRegistryOnline = 1 + // EdgeletRegistryAirgap is from_cache (edgelet registry ls ID 2) for pre-loaded images. + EdgeletRegistryAirgap = 2 +) + +// edgeletControlPlaneManifest mirrors edgelet ControlPlane YAML (edgelet.iofog.org/v1). +type edgeletControlPlaneManifest struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata edgeletControlPlaneMetadata `yaml:"metadata"` + Spec edgeletControlPlaneManifestSpec `yaml:"spec"` +} + +type edgeletControlPlaneMetadata struct { + Name string `yaml:"name"` + Namespace string `yaml:"namespace,omitempty"` +} + +type edgeletControlPlaneManifestSpec struct { + Controller edgeletControllerSpec `yaml:"controller"` + Console *edgeletConsoleSpec `yaml:"console,omitempty"` + Database *edgeletDatabaseSpec `yaml:"database,omitempty"` + Auth edgeletAuthSpec `yaml:"auth"` + Events *edgeletEventsSpec `yaml:"events,omitempty"` + SystemMicroservices *edgeletSystemMicroservices `yaml:"systemMicroservices,omitempty"` + Nats *edgeletNatsSpec `yaml:"nats,omitempty"` + LogLevel string `yaml:"logLevel,omitempty"` + TLS *edgeletTLSSpec `yaml:"tls,omitempty"` + Vault *edgeletVaultSpec `yaml:"vault,omitempty"` +} + +type edgeletControllerSpec struct { + Image string `yaml:"image"` + Registry *int `yaml:"registry,omitempty"` + Port *int `yaml:"port,omitempty"` + PublicURL string `yaml:"publicUrl,omitempty"` + TrustProxy *bool `yaml:"trustProxy,omitempty"` +} + +type edgeletConsoleSpec struct { + Port *int `yaml:"port,omitempty"` + URL string `yaml:"url,omitempty"` +} + +type edgeletDatabaseSpec struct { + Provider string `yaml:"provider,omitempty"` + User string `yaml:"user,omitempty"` + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` + Password string `yaml:"password,omitempty"` + DatabaseName string `yaml:"databaseName,omitempty"` + SSL *bool `yaml:"ssl,omitempty"` + CA *string `yaml:"ca,omitempty"` +} + +type edgeletAuthSpec struct { + Mode string `yaml:"mode"` + InsecureAllowHTTP *bool `yaml:"insecureAllowHttp,omitempty"` + InsecureAllowBootstrapLog *bool `yaml:"insecureAllowBootstrapLog,omitempty"` + Bootstrap *edgeletAuthBootstrap `yaml:"bootstrap,omitempty"` + IssuerURL string `yaml:"issuerUrl,omitempty"` + Client *edgeletAuthClient `yaml:"client,omitempty"` + ConsoleClient string `yaml:"consoleClient,omitempty"` + ConsoleClientEnabled *bool `yaml:"consoleClientEnabled,omitempty"` + RateLimit *edgeletAuthRateLimit `yaml:"rateLimit,omitempty"` + SessionStore *edgeletAuthSessionStore `yaml:"sessionStore,omitempty"` + TokenTTL *edgeletAuthTokenTTL `yaml:"tokenTtl,omitempty"` + OIDCTTL *edgeletAuthOIDCTTL `yaml:"oidcTtl,omitempty"` +} + +type edgeletAuthBootstrap struct { + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` +} + +type edgeletAuthClient struct { + ID string `yaml:"id,omitempty"` + Secret string `yaml:"secret,omitempty"` +} + +type edgeletAuthRateLimit struct { + Enabled *bool `yaml:"enabled,omitempty"` + MaxRequestsPerWindow int `yaml:"maxRequestsPerWindow,omitempty"` + WindowMs int `yaml:"windowMs,omitempty"` +} + +type edgeletAuthSessionStore struct { + Type string `yaml:"type,omitempty"` + TTLMs int `yaml:"ttlMs,omitempty"` + Secret string `yaml:"secret,omitempty"` +} + +type edgeletAuthTokenTTL struct { + AccessTokenTTLSeconds int `yaml:"accessTokenTtlSeconds,omitempty"` + RefreshTokenTTLSeconds int `yaml:"refreshTokenTtlSeconds,omitempty"` +} + +type edgeletAuthOIDCTTL struct { + InteractionTTLSeconds int `yaml:"interactionTtlSeconds,omitempty"` + GrantTTLSeconds int `yaml:"grantTtlSeconds,omitempty"` + SessionTTLSeconds int `yaml:"sessionTtlSeconds,omitempty"` + IDTokenTTLSeconds int `yaml:"idTokenTtlSeconds,omitempty"` +} + +type edgeletEventsSpec struct { + AuditEnabled *bool `yaml:"auditEnabled,omitempty"` + RetentionDays int `yaml:"retentionDays,omitempty"` + CleanupInterval int `yaml:"cleanupInterval,omitempty"` + CaptureIPAddress *bool `yaml:"captureIpAddress,omitempty"` +} + +type edgeletSystemMicroservices struct { + Router map[string]string `yaml:"router,omitempty"` + Nats map[string]string `yaml:"nats,omitempty"` +} + +type edgeletNatsSpec struct { + Enabled *bool `yaml:"enabled,omitempty"` +} + +type edgeletTLSBase64 struct { + CA string `yaml:"ca,omitempty"` + Cert string `yaml:"cert,omitempty"` + Key string `yaml:"key,omitempty"` +} + +type edgeletTLSSpec struct { + Base64 *edgeletTLSBase64 `yaml:"base64,omitempty"` +} + +type edgeletVaultSpec struct { + Enabled *bool `yaml:"enabled,omitempty"` + Provider string `yaml:"provider,omitempty"` + BasePath string `yaml:"basePath,omitempty"` + Hashicorp *edgeletVaultHashicorp `yaml:"hashicorp,omitempty"` + Aws *edgeletVaultAws `yaml:"aws,omitempty"` + Azure *edgeletVaultAzure `yaml:"azure,omitempty"` + Google *edgeletVaultGoogle `yaml:"google,omitempty"` +} + +type edgeletVaultHashicorp struct { + Address string `yaml:"address,omitempty"` + Token string `yaml:"token,omitempty"` + Mount string `yaml:"mount,omitempty"` +} + +type edgeletVaultAws struct { + Region string `yaml:"region,omitempty"` + AccessKeyID string `yaml:"accessKeyId,omitempty"` + AccessKey string `yaml:"accessKey,omitempty"` +} + +type edgeletVaultAzure struct { + URL string `yaml:"url,omitempty"` + TenantID string `yaml:"tenantId,omitempty"` + ClientID string `yaml:"clientId,omitempty"` + ClientSecret string `yaml:"clientSecret,omitempty"` +} + +type edgeletVaultGoogle struct { + ProjectID string `yaml:"projectId,omitempty"` + Credentials string `yaml:"credentials,omitempty"` +} + +type edgeletRegistryManifest struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Spec edgeletRegistryManifestSpec `yaml:"spec"` +} + +type edgeletRegistryManifestSpec struct { + URL string `yaml:"url"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + Email string `yaml:"email,omitempty"` + Private bool `yaml:"private"` +} diff --git a/internal/deploy/controlplane/local/system_agent.go b/internal/deploy/controlplane/local/system_agent.go new file mode 100644 index 000000000..62facdc99 --- /dev/null +++ b/internal/deploy/controlplane/local/system_agent.go @@ -0,0 +1,143 @@ +package deploylocalcontrolplane + +import ( + "context" + "fmt" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + deployagentconfig "github.com/eclipse-iofog/iofogctl/internal/deploy/agentconfig" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + iutil "github.com/eclipse-iofog/iofogctl/internal/util" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const ( + deploymentTypeNative = "native" + deploymentTypeContainer = "container" +) + +func buildLocalSystemAgentConfig(cp *rsc.LocalControlPlane, name string) (rsc.AgentConfiguration, error) { + sys := cp.SystemAgent + if sys == nil { + return rsc.AgentConfiguration{}, util.NewInputError("Local Control Plane systemAgent is required") + } + + var deployAgentConfig rsc.AgentConfiguration + if sys.AgentConfiguration != nil { + deployAgentConfig = *sys.AgentConfiguration + } + + deploymentType := deploymentTypeNative + if deployAgentConfig.DeploymentType != nil { + deploymentType = *deployAgentConfig.DeploymentType + } else if sys.Package.Container.Image != "" { + deploymentType = deploymentTypeContainer + } + + if deployAgentConfig.Host == nil || strings.TrimSpace(*deployAgentConfig.Host) == "" { + ip, err := util.DetectLocalHostIPv4() + if err != nil { + return rsc.AgentConfiguration{}, util.NewInputError("Local Control Plane systemAgent requires spec.config.host or a detectable local IPv4 address") + } + deployAgentConfig.Host = &ip + } + + deployAgentConfig.IsSystem = iutil.MakeBoolPtr(true) + if deployAgentConfig.DeploymentType == nil { + deployAgentConfig.DeploymentType = iutil.MakeStrPtr(deploymentType) + } + + if deployAgentConfig.UpstreamRouters == nil { + emptyRouters := []string{} + deployAgentConfig.UpstreamRouters = &emptyRouters + } + if deployAgentConfig.UpstreamNatsServers == nil { + emptyNats := []string{} + deployAgentConfig.UpstreamNatsServers = &emptyNats + } + + deployagentconfig.ApplySystemAgentDefaults(&deployAgentConfig) + + if deployAgentConfig.Name == "" { + deployAgentConfig.Name = name + } + + return deployAgentConfig, nil +} + +func deployLocalSystemAgent(namespace string, cp *rsc.LocalControlPlane, name string, edgelet *install.LocalEdgelet) error { + install.Verbose("Deploying system agent for local control plane " + name) + + deployAgentConfig, err := buildLocalSystemAgentConfig(cp, name) + if err != nil { + return err + } + + configExe := deployagentconfig.NewRemoteExecutor(name, &deployAgentConfig, namespace, nil) + if err := configExe.Execute(); err != nil { + return fmt.Errorf("failed to deploy system agent configuration: %w", err) + } + + user := install.IofogUser(cp.GetUser()) + user.Password = cp.GetUser().GetRawPassword() + + endpoint, err := cp.GetEndpoint() + if err != nil { + return err + } + + opt, err := clientutil.ControllerClientOptions(context.Background(), namespace, endpoint) + if err != nil { + return err + } + + if _, err := edgelet.Configure(endpoint, user, opt); err != nil { + return fmt.Errorf("failed to provision system agent: %w", err) + } + return persistLocalSystemAgent(namespace, cp, name, deployAgentConfig, endpoint, configExe.GetAgentUUID()) +} + +func persistLocalSystemAgent(namespace string, cp *rsc.LocalControlPlane, name string, deployAgentConfig rsc.AgentConfiguration, endpoint, uuid string) error { + ns, err := config.GetNamespace(namespace) + if err != nil { + return err + } + + host := deployAgentConfig.Host + if host == nil || strings.TrimSpace(*host) == "" { + var agentInfo *client.AgentInfo + err = clientutil.ExecuteWithAuthRetry(namespace, func(clt *client.Client) error { + var err error + agentInfo, err = clt.GetAgentByName(name) + return err + }) + if err != nil { + return fmt.Errorf("failed to load system agent from controller: %w", err) + } + host = &agentInfo.Host + } + + configCopy := deployAgentConfig + agent := &rsc.LocalAgent{ + Name: name, + UUID: uuid, + Created: util.NowUTC(), + Host: *host, + ControllerEndpoint: endpoint, + Airgap: cp.Airgap, + Config: &configCopy, + } + if cp.SystemAgent != nil { + agent.Package = cp.SystemAgent.Package + agent.Scripts = cp.SystemAgent.Scripts + } + + if err := ns.UpdateAgent(agent); err != nil { + return err + } + return config.Flush() +} diff --git a/internal/deploy/controlplane/local/system_agent_test.go b/internal/deploy/controlplane/local/system_agent_test.go new file mode 100644 index 000000000..ed1a7f4c8 --- /dev/null +++ b/internal/deploy/controlplane/local/system_agent_test.go @@ -0,0 +1,56 @@ +package deploylocalcontrolplane + +import ( + "testing" + + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + iutil "github.com/eclipse-iofog/iofogctl/internal/util" + "github.com/eclipse-iofog/iofogctl/pkg/iofog" + "github.com/stretchr/testify/require" +) + +func TestBuildLocalSystemAgentConfig_ForcesSystemDefaults(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + cfg, err := buildLocalSystemAgentConfig(&cp, "iofog") + require.NoError(t, err) + + require.NotNil(t, cfg.IsSystem) + require.True(t, *cfg.IsSystem) + require.NotNil(t, cfg.RouterMode) + require.Equal(t, iofog.RouterModeInterior, *cfg.RouterMode) + require.NotNil(t, cfg.DeploymentType) + require.Equal(t, deploymentTypeNative, *cfg.DeploymentType) + require.NotNil(t, cfg.UpstreamRouters) + require.Empty(t, *cfg.UpstreamRouters) + require.NotNil(t, cfg.NatsMode) + require.Equal(t, iofog.NatsModeServer, *cfg.NatsMode) + require.Equal(t, "iofog", cfg.Name) + require.NotNil(t, cfg.Host) + require.NotEmpty(t, *cfg.Host) +} + +func TestBuildLocalSystemAgentConfig_OverridesFalseIsSystem(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + cp.SystemAgent.AgentConfiguration.IsSystem = iutil.MakeBoolPtr(false) + + cfg, err := buildLocalSystemAgentConfig(&cp, "iofog") + require.NoError(t, err) + require.True(t, *cfg.IsSystem) +} + +func TestResolveLocalControlPlaneEndpoint(t *testing.T) { + cp := rsc.LocalControlPlane{ + Endpoint: "https://cp.example.com", + } + require.Equal(t, "https://cp.example.com", resolveLocalControlPlaneEndpoint(&cp)) + + cp = rsc.LocalControlPlane{ + Controller: rsc.LocalControllerSpec{ + ControllerConfig: rsc.ControllerConfig{PublicUrl: "https://public.example.com"}, + }, + } + require.Equal(t, "https://public.example.com", resolveLocalControlPlaneEndpoint(&cp)) + + cp = rsc.LocalControlPlane{} + require.Equal(t, "http://localhost:51121", resolveLocalControlPlaneEndpoint(&cp)) +} diff --git a/internal/deploy/controlplane/local/testdata/edgelet-cp-datasance.yaml b/internal/deploy/controlplane/local/testdata/edgelet-cp-datasance.yaml new file mode 100644 index 000000000..5ad54a962 --- /dev/null +++ b/internal/deploy/controlplane/local/testdata/edgelet-cp-datasance.yaml @@ -0,0 +1,37 @@ +apiVersion: edgelet.iofog.org/v1 +kind: ControlPlane +metadata: + name: iofog + namespace: test-ns +spec: + controller: + image: ghcr.io/datasance/controller:3.8.0-rc.1 + registry: 1 + publicUrl: https://controller.example.com + console: + url: https://controller.example.com + auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true + systemMicroservices: + router: + amd64: ghcr.io/datasance/router:3.8.0-rc.1 + arm64: ghcr.io/datasance/router:3.8.0-rc.1 + riscv64: ghcr.io/datasance/router:3.8.0-rc.1 + arm: ghcr.io/datasance/router:3.8.0-rc.1 + nats: + amd64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm64: ghcr.io/datasance/nats:2.14.2-rc.2 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm: ghcr.io/datasance/nats:2.14.2-rc.2 + nats: + enabled: true + logLevel: info diff --git a/internal/deploy/controlplane/local/testdata/edgelet-cp-iofog.yaml b/internal/deploy/controlplane/local/testdata/edgelet-cp-iofog.yaml new file mode 100644 index 000000000..a0986b0d1 --- /dev/null +++ b/internal/deploy/controlplane/local/testdata/edgelet-cp-iofog.yaml @@ -0,0 +1,35 @@ +apiVersion: edgelet.iofog.org/v1 +kind: ControlPlane +metadata: + name: iofog + namespace: test-ns +spec: + controller: + image: ghcr.io/eclipse-iofog/controller:3.8.0-rc.1 + registry: 1 + publicUrl: https://controller.example.com + console: + url: https://controller.example.com + auth: + mode: embedded + bootstrap: + username: admin + events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true + systemMicroservices: + router: + amd64: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + arm64: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + riscv64: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + arm: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + nats: + amd64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + arm64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + riscv64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + arm: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + nats: + enabled: true + logLevel: info diff --git a/internal/deploy/controlplane/local/testdata/edgelet-registry-private.yaml b/internal/deploy/controlplane/local/testdata/edgelet-registry-private.yaml new file mode 100644 index 000000000..70ca52fa1 --- /dev/null +++ b/internal/deploy/controlplane/local/testdata/edgelet-registry-private.yaml @@ -0,0 +1,8 @@ +apiVersion: edgelet.iofog.org/v1 +kind: Registry +spec: + url: quay.io + username: john + password: secret-token + email: user@domain.com + private: true diff --git a/internal/deploy/controlplane/local/translate.go b/internal/deploy/controlplane/local/translate.go new file mode 100644 index 000000000..a10defb02 --- /dev/null +++ b/internal/deploy/controlplane/local/translate.go @@ -0,0 +1,393 @@ +package deploylocalcontrolplane + +import ( + "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" +) + +// TranslateOptions configures LocalControlPlane → edgelet manifest translation. +type TranslateOptions struct { + Name string + Namespace string + ControllerImage string + RouterImage string + NatsImage string + RegistryID *int +} + +// TranslateResult holds optional edgelet Registry YAML and required ControlPlane YAML. +type TranslateResult struct { + Registry []byte + ControlPlane []byte +} + +type translateOptions struct { + name string + namespace string + controllerImage string + routerImage string + natsImage string + registryID *int +} + +func defaultTranslateOptions(name, namespace string) translateOptions { + return translateOptions{ + name: name, + namespace: namespace, + controllerImage: util.GetControllerImage(), + routerImage: util.GetRouterImage(), + natsImage: util.GetNatsImage(), + } +} + +func (o TranslateOptions) mergeDefaults(namespace string) translateOptions { + base := defaultTranslateOptions(o.Name, namespace) + if o.Name != "" { + base.name = o.Name + } + if o.Namespace != "" { + base.namespace = o.Namespace + } + if o.ControllerImage != "" { + base.controllerImage = o.ControllerImage + } + if o.RouterImage != "" { + base.routerImage = o.RouterImage + } + if o.NatsImage != "" { + base.natsImage = o.NatsImage + } + base.registryID = o.RegistryID + return base +} + +// NeedsPrivateEdgeletRegistry reports whether controller.package requires an edgelet Registry manifest. +func NeedsPrivateEdgeletRegistry(cp *resource.LocalControlPlane) bool { + pkg := cp.Controller.Package + if pkg == nil { + return false + } + return pkg.Registry != "" && pkg.Username != "" && pkg.Password != "" +} + +// ResolveEdgeletRegistryID picks spec.controller.registry for the edgelet ControlPlane manifest. +// Private registries use the ID from edgelet registry ls; otherwise online=1, airgap=2. +func ResolveEdgeletRegistryID(cp *resource.LocalControlPlane, privateRegistryID *int) *int { + if privateRegistryID != nil { + return privateRegistryID + } + id := EdgeletRegistryOnline + if cp != nil && cp.Airgap { + id = EdgeletRegistryAirgap + } + return &id +} + +// TranslateLocalControlPlane maps CLI LocalControlPlane to edgelet Registry (optional) and ControlPlane YAML. +func TranslateLocalControlPlane(cp *resource.LocalControlPlane, opts TranslateOptions) (TranslateResult, error) { + tOpts := opts.mergeDefaults(opts.Namespace) + var out TranslateResult + + if NeedsPrivateEdgeletRegistry(cp) { + reg, err := translateEdgeletRegistry(cp) + if err != nil { + return out, err + } + data, err := yaml.Marshal(reg) + if err != nil { + return out, err + } + out.Registry = data + } + + manifest := translateEdgeletControlPlane(cp, tOpts) + data, err := yaml.Marshal(manifest) + if err != nil { + return out, err + } + out.ControlPlane = data + return out, nil +} + +// TranslateEdgeletControlPlaneManifest returns the edgelet ControlPlane manifest struct (test helper). +// +//nolint:revive // test helper intentionally returns package-private manifest type +func TranslateEdgeletControlPlaneManifest(cp *resource.LocalControlPlane, opts TranslateOptions) edgeletControlPlaneManifest { + return translateEdgeletControlPlane(cp, opts.mergeDefaults(opts.Namespace)) +} + +// TranslateEdgeletRegistryManifest returns the edgelet Registry manifest struct (test helper). +// +//nolint:revive // test helper intentionally returns package-private manifest type +func TranslateEdgeletRegistryManifest(cp *resource.LocalControlPlane) (edgeletRegistryManifest, error) { + if !NeedsPrivateEdgeletRegistry(cp) { + return edgeletRegistryManifest{}, util.NewError("Local Control Plane does not require a private edgelet registry") + } + return translateEdgeletRegistry(cp) +} + +func translateEdgeletRegistry(cp *resource.LocalControlPlane) (edgeletRegistryManifest, error) { + pkg := cp.Controller.Package + return edgeletRegistryManifest{ + APIVersion: edgeletAPIVersion, + Kind: edgeletRegistryKind, + Spec: edgeletRegistryManifestSpec{ + URL: pkg.Registry, + Username: pkg.Username, + Password: pkg.Password, + Email: pkg.Email, + Private: true, + }, + }, nil +} + +func translateEdgeletControlPlane(cp *resource.LocalControlPlane, opts translateOptions) edgeletControlPlaneManifest { + spec := edgeletControlPlaneManifestSpec{ + Controller: edgeletControllerSpec{ + Image: controllerImage(cp, opts.controllerImage), + Registry: opts.registryID, + PublicURL: resolvePublicURL(cp), + TrustProxy: cp.Controller.TrustProxy, + }, + Auth: authToEdgelet(cp.Auth), + LogLevel: cp.Controller.LogLevel, + } + if console := consoleToEdgelet(cp.Controller); console != nil { + spec.Console = console + } + if db := databaseToEdgelet(cp.Database); db != nil { + spec.Database = db + } + if ev := eventsToEdgelet(cp.Events); ev != nil { + spec.Events = ev + } + if sys := systemMicroservicesToEdgelet(cp.SystemMicroservices, opts); sys != nil { + spec.SystemMicroservices = sys + } + if nats := natsToEdgelet(cp.Nats); nats != nil { + spec.Nats = nats + } + if tls := tlsToEdgelet(cp.TLS); tls != nil { + spec.TLS = tls + } + if vault := vaultToEdgelet(cp.Vault); vault != nil { + spec.Vault = vault + } + return edgeletControlPlaneManifest{ + APIVersion: edgeletAPIVersion, + Kind: edgeletControlPlaneKind, + Metadata: edgeletControlPlaneMetadata{ + Name: opts.name, + Namespace: opts.namespace, + }, + Spec: spec, + } +} + +func controllerImage(cp *resource.LocalControlPlane, defaultImage string) string { + if cp.Controller.Package != nil && cp.Controller.Package.Image != "" { + return cp.Controller.Package.Image + } + return defaultImage +} + +func resolvePublicURL(cp *resource.LocalControlPlane) string { + if cp.Controller.PublicUrl != "" { + return cp.Controller.PublicUrl + } + return cp.Endpoint +} + +func consoleToEdgelet(ctrl resource.LocalControllerSpec) *edgeletConsoleSpec { + if ctrl.ConsoleUrl == "" && ctrl.ConsolePort == 0 { + return nil + } + out := &edgeletConsoleSpec{URL: ctrl.ConsoleUrl} + if ctrl.ConsolePort != 0 { + out.Port = &ctrl.ConsolePort + } + return out +} + +func authToEdgelet(a resource.Auth) edgeletAuthSpec { + out := edgeletAuthSpec{ + Mode: a.Mode, + InsecureAllowHTTP: a.InsecureAllowHttp, + InsecureAllowBootstrapLog: a.InsecureAllowBootstrapLog, + IssuerURL: a.IssuerUrl, + ConsoleClient: a.ConsoleClient, + ConsoleClientEnabled: a.ConsoleClientEnabled, + } + if a.Bootstrap != nil { + out.Bootstrap = &edgeletAuthBootstrap{ + Username: a.Bootstrap.Username, + Password: a.Bootstrap.Password, + } + } + if a.Client != nil { + out.Client = &edgeletAuthClient{ + ID: a.Client.ID, + Secret: a.Client.Secret, + } + } + if a.RateLimit != nil { + out.RateLimit = &edgeletAuthRateLimit{ + Enabled: a.RateLimit.Enabled, + MaxRequestsPerWindow: a.RateLimit.MaxRequestsPerWindow, + WindowMs: a.RateLimit.WindowMs, + } + } + if a.SessionStore != nil { + out.SessionStore = &edgeletAuthSessionStore{ + Type: a.SessionStore.Type, + TTLMs: a.SessionStore.TtlMs, + Secret: a.SessionStore.Secret, + } + } + if a.TokenTtl != nil { + out.TokenTTL = &edgeletAuthTokenTTL{ + AccessTokenTTLSeconds: a.TokenTtl.AccessTokenTtlSeconds, + RefreshTokenTTLSeconds: a.TokenTtl.RefreshTokenTtlSeconds, + } + } + if a.OidcTtl != nil { + out.OIDCTTL = &edgeletAuthOIDCTTL{ + InteractionTTLSeconds: a.OidcTtl.InteractionTtlSeconds, + GrantTTLSeconds: a.OidcTtl.GrantTtlSeconds, + SessionTTLSeconds: a.OidcTtl.SessionTtlSeconds, + IDTokenTTLSeconds: a.OidcTtl.IdTokenTtlSeconds, + } + } + return out +} + +func databaseToEdgelet(db resource.Database) *edgeletDatabaseSpec { + if db.Provider == "" { + return nil + } + return &edgeletDatabaseSpec{ + Provider: db.Provider, + User: db.User, + Host: db.Host, + Port: db.Port, + Password: db.Password, + DatabaseName: db.DatabaseName, + SSL: db.SSL, + CA: db.CA, + } +} + +func eventsToEdgelet(ev resource.Events) *edgeletEventsSpec { + if ev.AuditEnabled == nil && ev.RetentionDays == 0 && ev.CleanupInterval == 0 && ev.CaptureIpAddress == nil { + return nil + } + return &edgeletEventsSpec{ + AuditEnabled: ev.AuditEnabled, + RetentionDays: ev.RetentionDays, + CleanupInterval: ev.CleanupInterval, + CaptureIPAddress: ev.CaptureIpAddress, + } +} + +func systemMicroservicesToEdgelet(sys install.RemoteSystemMicroservices, opts translateOptions) *edgeletSystemMicroservices { + router := remoteImagesToArchMap(sys.Router, opts.routerImage) + nats := remoteImagesToArchMap(sys.Nats, opts.natsImage) + if len(router) == 0 && len(nats) == 0 { + return nil + } + return &edgeletSystemMicroservices{ + Router: router, + Nats: nats, + } +} + +func remoteImagesToArchMap(images install.RemoteSystemImages, defaultImage string) map[string]string { + out := map[string]string{} + if images.AMD64 != "" { + out["amd64"] = images.AMD64 + } + if images.ARM64 != "" { + out["arm64"] = images.ARM64 + } + if images.RISCV64 != "" { + out["riscv64"] = images.RISCV64 + } + if images.ARM != "" { + out["arm"] = images.ARM + } + if len(out) == 0 && defaultImage != "" { + out = defaultArchImages(defaultImage) + } + return out +} + +func defaultArchImages(image string) map[string]string { + return map[string]string{ + "amd64": image, + "arm64": image, + "riscv64": image, + "arm": image, + } +} + +func natsToEdgelet(n *resource.NatsEnabledConfig) *edgeletNatsSpec { + if n == nil { + return nil + } + return &edgeletNatsSpec{Enabled: n.Enabled} +} + +func tlsToEdgelet(t *resource.ControlPlaneTLS) *edgeletTLSSpec { + if t == nil || (t.CA == "" && t.Cert == "" && t.Key == "") { + return nil + } + return &edgeletTLSSpec{ + Base64: &edgeletTLSBase64{ + CA: t.CA, + Cert: t.Cert, + Key: t.Key, + }, + } +} + +func vaultToEdgelet(v *resource.VaultSpec) *edgeletVaultSpec { + if v == nil { + return nil + } + out := &edgeletVaultSpec{ + Enabled: v.Enabled, + Provider: v.Provider, + BasePath: v.BasePath, + } + if v.Hashicorp != nil { + out.Hashicorp = &edgeletVaultHashicorp{ + Address: v.Hashicorp.Address, + Token: v.Hashicorp.Token, + Mount: v.Hashicorp.Mount, + } + } + if v.Aws != nil { + out.Aws = &edgeletVaultAws{ + Region: v.Aws.Region, + AccessKeyID: v.Aws.AccessKeyId, + AccessKey: v.Aws.AccessKey, + } + } + if v.Azure != nil { + out.Azure = &edgeletVaultAzure{ + URL: v.Azure.URL, + TenantID: v.Azure.TenantId, + ClientID: v.Azure.ClientId, + ClientSecret: v.Azure.ClientSecret, + } + } + if v.Google != nil { + out.Google = &edgeletVaultGoogle{ + ProjectID: v.Google.ProjectId, + Credentials: v.Google.Credentials, + } + } + return out +} diff --git a/internal/deploy/controlplane/local/translate_test.go b/internal/deploy/controlplane/local/translate_test.go new file mode 100644 index 000000000..e666e0b0a --- /dev/null +++ b/internal/deploy/controlplane/local/translate_test.go @@ -0,0 +1,212 @@ +package deploylocalcontrolplane + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" +) + +const testNamespace = "test-ns" + +const ( + testControllerImage = "ghcr.io/datasance/controller:3.8.0-rc.5" + testRouterImage = "ghcr.io/datasance/router:3.8.0-rc.1" + testNatsImage = "ghcr.io/datasance/nats:2.14.2-rc.2" + + testIofogControllerImage = "ghcr.io/eclipse-iofog/controller:3.8.0-rc.1" + testIofogRouterImage = "ghcr.io/eclipse-iofog/router:3.8.0-rc.1" + testIofogNatsImage = "ghcr.io/eclipse-iofog/nats:2.14.2-rc.2" +) + +func resourceFixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "..", "..", "..", "resource", "testdata", "local", name) +} + +func localFixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "testdata", name) +} + +func loadLocalControlPlaneFixture(t *testing.T, name string) rsc.LocalControlPlane { + t.Helper() + raw, err := os.ReadFile(resourceFixturePath(name)) + require.NoError(t, err) + cp, err := rsc.UnmarshallLocalControlPlane(raw) + require.NoError(t, err) + return cp +} + +func loadExpectedControlPlane(t *testing.T, name string) edgeletControlPlaneManifest { + t.Helper() + raw, err := os.ReadFile(localFixturePath(name)) + require.NoError(t, err) + var manifest edgeletControlPlaneManifest + require.NoError(t, yaml.UnmarshalStrict(raw, &manifest)) + return manifest +} + +func loadExpectedRegistry(t *testing.T, name string) edgeletRegistryManifest { + t.Helper() + raw, err := os.ReadFile(localFixturePath(name)) + require.NoError(t, err) + var manifest edgeletRegistryManifest + require.NoError(t, yaml.UnmarshalStrict(raw, &manifest)) + return manifest +} + +func stripManifestSecrets(m *edgeletControlPlaneManifest) { + if m.Spec.Auth.Bootstrap != nil { + m.Spec.Auth.Bootstrap.Password = "" + } +} + +func assertTranslatedControlPlane(t *testing.T, got, want edgeletControlPlaneManifest) { + t.Helper() + stripManifestSecrets(&got) + stripManifestSecrets(&want) + require.Equal(t, want, got) +} + +func datasanceTranslateOptions() TranslateOptions { + return TranslateOptions{ + Name: "iofog", + Namespace: testNamespace, + ControllerImage: testControllerImage, + RouterImage: testRouterImage, + NatsImage: testNatsImage, + } +} + +func iofogTranslateOptions() TranslateOptions { + return TranslateOptions{ + Name: "iofog", + Namespace: testNamespace, + ControllerImage: testIofogControllerImage, + RouterImage: testIofogRouterImage, + NatsImage: testIofogNatsImage, + } +} + +func TestTranslateEdgeletControlPlane_DatasanceGolden(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + got := TranslateEdgeletControlPlaneManifest(&cp, datasanceTranslateOptions()) + want := loadExpectedControlPlane(t, "edgelet-cp-datasance.yaml") + assertTranslatedControlPlane(t, got, want) +} + +func TestTranslateEdgeletControlPlane_IofogGolden(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-iofog.yaml") + got := TranslateEdgeletControlPlaneManifest(&cp, iofogTranslateOptions()) + want := loadExpectedControlPlane(t, "edgelet-cp-iofog.yaml") + assertTranslatedControlPlane(t, got, want) +} + +func TestTranslateEdgeletRegistry_PrivateGolden(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-private-registry.yaml") + require.True(t, NeedsPrivateEdgeletRegistry(&cp)) + got, err := TranslateEdgeletRegistryManifest(&cp) + require.NoError(t, err) + want := loadExpectedRegistry(t, "edgelet-registry-private.yaml") + require.Equal(t, want, got) +} + +func TestTranslateLocalControlPlane_WithRegistryID(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-private-registry.yaml") + registryID := 3 + opts := datasanceTranslateOptions() + opts.RegistryID = ®istryID + + got := TranslateEdgeletControlPlaneManifest(&cp, opts) + require.NotNil(t, got.Spec.Controller.Registry) + require.Equal(t, 3, *got.Spec.Controller.Registry) + + result, err := TranslateLocalControlPlane(&cp, opts) + require.NoError(t, err) + require.NotEmpty(t, result.Registry) + require.NotEmpty(t, result.ControlPlane) +} + +func TestTranslateEdgeletControlPlane_DefaultControllerImage(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + cp.Controller.Package = nil + got := TranslateEdgeletControlPlaneManifest(&cp, datasanceTranslateOptions()) + require.Equal(t, testControllerImage, got.Spec.Controller.Image) +} + +func TestTranslateEdgeletControlPlane_StripsCLIOnlyFields(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + result, err := TranslateLocalControlPlane(&cp, datasanceTranslateOptions()) + require.NoError(t, err) + require.Nil(t, result.Registry) + body := string(result.ControlPlane) + require.NotContains(t, body, "iofogUser") + require.NotContains(t, body, "systemAgent") + require.NotContains(t, body, "endpoint:") +} + +func TestTranslateEdgeletControlPlane_LogLevelTopLevel(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + got := TranslateEdgeletControlPlaneManifest(&cp, datasanceTranslateOptions()) + require.Equal(t, "info", got.Spec.LogLevel) + require.NotEmpty(t, got.Spec.Controller.Image) +} + +func TestTranslateEdgeletControlPlane_ConsoleReshape(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + got := TranslateEdgeletControlPlaneManifest(&cp, datasanceTranslateOptions()) + require.NotNil(t, got.Spec.Console) + require.Equal(t, "https://controller.example.com", got.Spec.Console.URL) +} + +func TestNeedsPrivateEdgeletRegistry(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + require.False(t, NeedsPrivateEdgeletRegistry(&cp)) + + privateCP := loadLocalControlPlaneFixture(t, "controlplane-private-registry.yaml") + require.True(t, NeedsPrivateEdgeletRegistry(&privateCP)) +} + +func TestTranslateEdgeletControlPlane_PublicURLFromEndpoint(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + cp.Controller.PublicUrl = "" + got := TranslateEdgeletControlPlaneManifest(&cp, datasanceTranslateOptions()) + require.Equal(t, "https://controller.example.com", got.Spec.Controller.PublicURL) +} + +func TestResolveEdgeletRegistryID_OnlineDefault(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + got := ResolveEdgeletRegistryID(&cp, nil) + require.NotNil(t, got) + require.Equal(t, EdgeletRegistryOnline, *got) +} + +func TestResolveEdgeletRegistryID_AirgapDefault(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + cp.Airgap = true + got := ResolveEdgeletRegistryID(&cp, nil) + require.NotNil(t, got) + require.Equal(t, EdgeletRegistryAirgap, *got) +} + +func TestResolveEdgeletRegistryID_PrivateRegistry(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + privateID := 3 + got := ResolveEdgeletRegistryID(&cp, &privateID) + require.Equal(t, 3, *got) +} + +func TestTranslateEdgeletControlPlane_AirgapRegistry(t *testing.T) { + cp := loadLocalControlPlaneFixture(t, "controlplane-datasance.yaml") + cp.Airgap = true + opts := datasanceTranslateOptions() + opts.RegistryID = ResolveEdgeletRegistryID(&cp, nil) + got := TranslateEdgeletControlPlaneManifest(&cp, opts) + require.NotNil(t, got.Spec.Controller.Registry) + require.Equal(t, EdgeletRegistryAirgap, *got.Spec.Controller.Registry) +} diff --git a/internal/deploy/controlplane/remote/certificates.go b/internal/deploy/controlplane/remote/certificates.go new file mode 100644 index 000000000..2477fe4e8 --- /dev/null +++ b/internal/deploy/controlplane/remote/certificates.go @@ -0,0 +1,46 @@ +package deployremotecontrolplane + +import ( + "github.com/eclipse-iofog/iofogctl/internal/execute" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" +) + +func runExecutors(executors []execute.Executor) error { + if errs, _ := execute.ForParallel(executors); len(errs) > 0 { + return execute.CoalesceErrors(errs) + } + return nil +} + +func globalCertificatesFromCP(cp *rsc.RemoteControlPlane) install.GlobalCertificates { + if cp == nil { + return install.GlobalCertificates{} + } + out := install.GlobalCertificates{} + if cp.RouterSiteCA != nil { + out.RouterSiteCA = &install.SiteCertificate{ + TLSCert: cp.RouterSiteCA.TLSCert, + TLSKey: cp.RouterSiteCA.TLSKey, + } + } + if cp.RouterLocalCA != nil { + out.RouterLocalCA = &install.SiteCertificate{ + TLSCert: cp.RouterLocalCA.TLSCert, + TLSKey: cp.RouterLocalCA.TLSKey, + } + } + if cp.NatsSiteCA != nil { + out.NatsSiteCA = &install.SiteCertificate{ + TLSCert: cp.NatsSiteCA.TLSCert, + TLSKey: cp.NatsSiteCA.TLSKey, + } + } + if cp.NatsLocalCA != nil { + out.NatsLocalCA = &install.SiteCertificate{ + TLSCert: cp.NatsLocalCA.TLSCert, + TLSKey: cp.NatsLocalCA.TLSKey, + } + } + return out +} diff --git a/internal/deploy/controlplane/remote/edgelet_host.go b/internal/deploy/controlplane/remote/edgelet_host.go new file mode 100644 index 000000000..e77ef9ffd --- /dev/null +++ b/internal/deploy/controlplane/remote/edgelet_host.go @@ -0,0 +1,316 @@ +package deployremotecontrolplane + +import ( + "context" + "fmt" + "net" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +// BuildRemoteEdgelet constructs a RemoteEdgelet from per-controller systemAgent settings. +func BuildRemoteEdgelet(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, agentUUID string) (*install.RemoteEdgelet, error) { + sys := ctrl.SystemAgent + var cfg *rsc.AgentConfiguration + var pkg rsc.Package + var scripts *rsc.AgentScripts + if sys != nil { + cfg = sys.AgentConfiguration + pkg = sys.Package + scripts = sys.Scripts + } + + cfg = deployairgap.EnsureAgentConfig(cfg) + deployairgap.ResolveAgentDeployment(cfg, pkg.Container.Image) + + installCfg := deployairgap.EdgeletInstallConfig("linux", cfg, pkg) + edgelet, err := install.NewRemoteEdgelet( + ctrl.SSH.User, + ctrl.Host, + ctrl.SSH.Port, + ctrl.SSH.KeyFile, + ctrl.Name, + agentUUID, + installCfg, + ) + if err != nil { + return nil, err + } + + if scripts != nil { + procs := install.EdgeletProcedures{AgentProcedures: scripts.AgentProcedures} + if err := edgelet.CustomizeProcedures(scripts.Directory, &procs); err != nil { + return nil, err + } + } + if pkg.Container.Image != "" { + if err := edgelet.SetContainerImage(pkg.Container.Image); err != nil { + return nil, err + } + } else if pkg.Version != "" { + if err := edgelet.SetVersion(pkg.Version); err != nil { + return nil, err + } + } + return edgelet, nil +} + +func isNonRoutableControllerHost(host string) bool { + host = strings.TrimSpace(host) + if host == "" { + return true + } + if h, _, err := net.SplitHostPort(host); err == nil { + host = h + } + host = strings.Trim(host, "[]") + switch strings.ToLower(host) { + case "0.0.0.0", "127.0.0.1", "localhost", "::1": + return true + default: + return false + } +} + +func systemAgentConfigHost(ctrl *rsc.RemoteController) string { + if ctrl == nil || ctrl.SystemAgent == nil || ctrl.SystemAgent.AgentConfiguration == nil { + return "" + } + if host := ctrl.SystemAgent.AgentConfiguration.Host; host != nil { + if h := strings.TrimSpace(*host); h != "" && !isNonRoutableControllerHost(h) { + return h + } + } + return "" +} + +func resolveControllerAPIHost(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController) string { + if cp != nil { + if publicURL := strings.TrimSpace(cp.Controller.PublicUrl); publicURL != "" { + if u, err := util.GetBaseURL(publicURL); err == nil && u.Host != "" { + return u.Host + } + } + } + if host := systemAgentConfigHost(ctrl); host != "" { + return host + } + if !isNonRoutableControllerHost(ctrl.Host) { + return strings.TrimSpace(ctrl.Host) + } + if cp != nil { + if cpEndpoint := strings.TrimSpace(cp.Endpoint); cpEndpoint != "" { + if u, err := util.GetBaseURL(cpEndpoint); err == nil && u.Host != "" && !isNonRoutableControllerHost(u.Hostname()) { + return u.Host + } + } + } + return strings.TrimSpace(ctrl.Host) +} + +func ResolveControllerHostEndpoint(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController) (string, error) { + if cp != nil { + if publicURL := strings.TrimSpace(cp.Controller.PublicUrl); publicURL != "" { + return publicURL, nil + } + } + + tls := EffectiveControllerTLS(cp, ctrl) + useHTTPS := tls != nil && tls.Cert != "" && tls.Key != "" + apiHost := resolveControllerAPIHost(cp, ctrl) + return util.GetControllerEndpoint(apiHost, useHTTPS) +} + +func DeployHostEdgelet(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, namespace string) (*install.RemoteEdgelet, error) { + edgelet, err := BuildRemoteEdgelet(cp, ctrl, "") + if err != nil { + return nil, err + } + + if cp.Airgap { + if err := transferControllerHostAirgap(context.Background(), namespace, cp, ctrl, edgelet); err != nil { + return nil, err + } + } + + util.SpinStart("Installing edgelet on " + ctrl.Name) + if err := edgelet.Bootstrap(); err != nil { + return nil, err + } + return edgelet, nil +} + +func transferControllerHostAirgap(ctx context.Context, namespace string, cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, edgelet *install.RemoteEdgelet) error { + if ctrl.SystemAgent == nil || ctrl.SystemAgent.AgentConfiguration == nil { + return util.NewInputError("systemAgent.config is required for airgap deployment on controller " + ctrl.Name) + } + + isInitial, err := deployairgap.IsInitialDeployment(namespace) + if err != nil { + return fmt.Errorf("failed to determine deployment type: %w", err) + } + + images, err := deployairgap.CollectControllerImages(namespace, cp, isInitial) + if err != nil { + return fmt.Errorf("failed to collect controller images: %w", err) + } + + platform, err := deployairgap.ResolvePlatform(ctrl.SystemAgent.AgentConfiguration.Arch) + if err != nil { + return fmt.Errorf("controller %s: %w", ctrl.Name, err) + } + opts, err := deployairgap.ControllerAirgapLoadOptions(ctrl.SystemAgent.AgentConfiguration) + if err != nil { + return fmt.Errorf("controller %s: %w", ctrl.Name, err) + } + + imageList := []string{images.Controller} + for _, img := range []string{images.NatsAMD64, images.NatsARM64, images.NatsRISCV64, images.NatsARM} { + if img != "" { + imageList = append(imageList, img) + } + } + + if deployairgap.IsNativeDeployment(opts.DeploymentType) { + remoteBinPath, err := deployairgap.TransferAgentAirgapBinary(ctx, namespace, ctrl.Host, &ctrl.SSH, platform) + if err != nil { + return fmt.Errorf("failed to transfer edgelet binary to %s: %w", ctrl.Name, err) + } + if err := edgelet.SetAirgap(remoteBinPath); err != nil { + return fmt.Errorf("failed to configure edgelet airgap binary on %s: %w", ctrl.Name, err) + } + } + + if len(imageList) > 0 { + if err := deployairgap.TransferAirgapImages(ctx, namespace, ctrl.Host, &ctrl.SSH, platform, opts, imageList); err != nil { + return fmt.Errorf("failed to transfer images to controller %s: %w", ctrl.Name, err) + } + } + return nil +} + +func DeployPrivateEdgeletRegistry(cp *rsc.RemoteControlPlane, edgelet *install.RemoteEdgelet, opts TranslateOptions) (*int, error) { + if !NeedsPrivateEdgeletRegistry(cp) { + return nil, nil + } + + result, err := TranslateRemoteControlPlane(cp, nil, opts) + if err != nil { + return nil, err + } + if len(result.Registry) == 0 { + return nil, util.NewError("private registry translation produced no manifest") + } + + path, cleanup, err := edgelet.WriteDeployManifest(result.Registry, "edgelet-registry") + if err != nil { + return nil, err + } + defer cleanup() + + if err := edgelet.DeployFromFile(path); err != nil { + return nil, fmt.Errorf("edgelet registry deploy failed: %w", err) + } + + output, err := edgelet.RegistryList() + if err != nil { + return nil, fmt.Errorf("edgelet registry ls failed: %w", err) + } + + pkg := cp.Controller.Package + id, err := install.ParseEdgeletRegistryID(output, pkg.Registry, pkg.Username) + if err != nil { + return nil, err + } + return &id, nil +} + +func DeployEdgeletControlPlane(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, edgelet *install.RemoteEdgelet, opts TranslateOptions, registryID *int) error { + opts.RegistryID = ResolveEdgeletRegistryID(cp, registryID) + result, err := TranslateRemoteControlPlane(cp, ctrl, opts) + if err != nil { + return err + } + + path, cleanup, err := edgelet.WriteDeployManifest(result.ControlPlane, "edgelet-controlplane") + if err != nil { + return err + } + defer cleanup() + + if err := edgelet.DeployFromFile(path); err != nil { + return fmt.Errorf("edgelet control plane deploy failed: %w", err) + } + return nil +} + +func deployControllerRegistry(namespace string, cp *rsc.RemoteControlPlane) error { + if !NeedsPrivateEdgeletRegistry(cp) { + return nil + } + pkg := cp.Controller.Package + if pkg == nil { + return nil + } + + clt, err := clientutil.NewControllerClient(namespace) + if err != nil { + return err + } + + email := pkg.Email + if email == "" { + email = "registry@local" + } + + createRequest := &client.RegistryCreateRequest{ + URL: pkg.Registry, + IsPublic: false, + Username: pkg.Username, + Password: pkg.Password, + Email: email, + } + if _, err = clt.CreateRegistry(createRequest); err != nil { + return fmt.Errorf("failed to register private registry with controller: %w", err) + } + return nil +} + +func deployRemoteControlPlaneHost(namespace, name string, cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController) error { + if err := ctrl.ValidateSSH(); err != nil { + return err + } + + edgelet, err := DeployHostEdgelet(cp, ctrl, namespace) + if err != nil { + return err + } + + translateOpts := TranslateOptions{ + Name: name, + Namespace: namespace, + } + + registryID, err := DeployPrivateEdgeletRegistry(cp, edgelet, translateOpts) + if err != nil { + return err + } + + if err := DeployEdgeletControlPlane(cp, ctrl, edgelet, translateOpts, registryID); err != nil { + return err + } + + endpoint, err := ResolveControllerHostEndpoint(cp, ctrl) + if err != nil { + return err + } + ctrl.Endpoint = endpoint + ctrl.Created = util.NowUTC() + return nil +} diff --git a/internal/deploy/controlplane/remote/edgelet_host_test.go b/internal/deploy/controlplane/remote/edgelet_host_test.go new file mode 100644 index 000000000..23dc1193d --- /dev/null +++ b/internal/deploy/controlplane/remote/edgelet_host_test.go @@ -0,0 +1,60 @@ +package deployremotecontrolplane + +import ( + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + iutil "github.com/eclipse-iofog/iofogctl/internal/util" + "github.com/stretchr/testify/require" +) + +func TestResolveControllerHostEndpointPrefersPublicURL(t *testing.T) { + cp := &rsc.RemoteControlPlane{ + Controller: rsc.LocalControllerSpec{ + ControllerConfig: rsc.ControllerConfig{ + PublicUrl: "http://192.168.139.85:51121", + }, + }, + } + ctrl := &rsc.RemoteController{Name: "remote-1", Host: "0.0.0.0"} + + endpoint, err := ResolveControllerHostEndpoint(cp, ctrl) + require.NoError(t, err) + require.Equal(t, "http://192.168.139.85:51121", endpoint) +} + +func TestResolveControllerHostEndpointUsesSystemAgentHostWhenSSHHostIsBindAddress(t *testing.T) { + cp := &rsc.RemoteControlPlane{} + ctrl := &rsc.RemoteController{ + Name: "remote-1", + Host: "0.0.0.0", + SystemAgent: &rsc.SystemAgentConfig{ + AgentConfiguration: &rsc.AgentConfiguration{ + AgentConfiguration: client.AgentConfiguration{ + Host: iutil.MakeStrPtr("192.168.139.85"), + }, + }, + }, + } + + endpoint, err := ResolveControllerHostEndpoint(cp, ctrl) + require.NoError(t, err) + require.Equal(t, "http://192.168.139.85:51121", endpoint) +} + +func TestResolveControllerHostEndpointUsesRoutableControllerHost(t *testing.T) { + cp := &rsc.RemoteControlPlane{} + ctrl := &rsc.RemoteController{Name: "remote-1", Host: "10.0.0.5"} + + endpoint, err := ResolveControllerHostEndpoint(cp, ctrl) + require.NoError(t, err) + require.Equal(t, "http://10.0.0.5:51121", endpoint) +} + +func TestIsNonRoutableControllerHost(t *testing.T) { + require.True(t, isNonRoutableControllerHost("0.0.0.0")) + require.True(t, isNonRoutableControllerHost("127.0.0.1")) + require.True(t, isNonRoutableControllerHost("localhost")) + require.False(t, isNonRoutableControllerHost("192.168.139.85")) +} diff --git a/internal/deploy/controlplane/remote/execute.go b/internal/deploy/controlplane/remote/execute.go index bc3977951..8eb284ab5 100644 --- a/internal/deploy/controlplane/remote/execute.go +++ b/internal/deploy/controlplane/remote/execute.go @@ -1,86 +1,20 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployremotecontrolplane import ( "context" "fmt" - "net" - "net/url" - "strings" - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/auth" "github.com/eclipse-iofog/iofogctl/internal/config" - deployagent "github.com/eclipse-iofog/iofogctl/internal/deploy/agent" - deployagentconfig "github.com/eclipse-iofog/iofogctl/internal/deploy/agentconfig" deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" - deployremotecontroller "github.com/eclipse-iofog/iofogctl/internal/deploy/controller/remote" "github.com/eclipse-iofog/iofogctl/internal/execute" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - iutil "github.com/eclipse-iofog/iofogctl/internal/util" - - // clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - - "github.com/eclipse-iofog/iofogctl/pkg/iofog" + "github.com/eclipse-iofog/iofogctl/internal/trust" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) -const ( - deploymentTypeContainer = "container" - deploymentTypeNative = "native" - - defaultNatsServerPort = 4222 - defaultNatsClusterPort = 6222 - defaultNatsLeafPort = 7422 - defaultNatsMqttPort = 8883 - defaultNatsHttpPort = 8222 - defaultJsStorageSize = "10G" - defaultJsMemoryStoreSize = "1G" -) - -// applySystemAgentNatsDefaults sets NATS config defaults for system agents when not provided (natsMode=server, ports, JsStorageSize, JsMemoryStoreSize). -func applySystemAgentNatsDefaults(cfg *rsc.AgentConfiguration) { - if cfg.NatsConfig.NatsMode == nil { - cfg.NatsConfig.NatsMode = iutil.MakeStrPtr(iofog.NatsModeServer) - } else { - // Force server mode for system agents (like router interior) - cfg.NatsConfig.NatsMode = iutil.MakeStrPtr(iofog.NatsModeServer) - } - if cfg.NatsConfig.NatsServerPort == nil { - cfg.NatsConfig.NatsServerPort = iutil.MakeIntPtr(defaultNatsServerPort) - } - if cfg.NatsConfig.NatsClusterPort == nil { - cfg.NatsConfig.NatsClusterPort = iutil.MakeIntPtr(defaultNatsClusterPort) - } - if cfg.NatsConfig.NatsLeafPort == nil { - cfg.NatsConfig.NatsLeafPort = iutil.MakeIntPtr(defaultNatsLeafPort) - } - if cfg.NatsConfig.NatsMqttPort == nil { - cfg.NatsMqttPort = iutil.MakeIntPtr(defaultNatsMqttPort) - } - if cfg.NatsConfig.NatsHttpPort == nil { - cfg.NatsConfig.NatsHttpPort = iutil.MakeIntPtr(defaultNatsHttpPort) - } - if cfg.NatsConfig.JsStorageSize == nil { - cfg.NatsConfig.JsStorageSize = iutil.MakeStrPtr(defaultJsStorageSize) - } - if cfg.NatsConfig.JsMemoryStoreSize == nil { - cfg.NatsConfig.JsMemoryStoreSize = iutil.MakeStrPtr(defaultJsMemoryStoreSize) - } -} - type Options struct { Namespace string Yaml []byte @@ -88,847 +22,152 @@ type Options struct { } type remoteControlPlaneExecutor struct { - ctrlClient *client.Client - controllerExecutors []execute.Executor - controlPlane rsc.ControlPlane - ns *rsc.Namespace - name string + controlPlane *rsc.RemoteControlPlane + namespace string + name string } -func deploySystemAgent(namespace string, ctrl *rsc.RemoteController, systemAgentConfig *rsc.SystemAgentConfig) (err error) { - // Deploy system agent to host internal router - install.Verbose("Deploying system agent for controller " + ctrl.Name) - // If DeploymentType is nil, default to "container" - var deploymentType string - if systemAgentConfig != nil && systemAgentConfig.AgentConfiguration != nil && systemAgentConfig.AgentConfiguration.AgentConfiguration.DeploymentType != nil { - // Use DeploymentType from provided configuration - deploymentType = *systemAgentConfig.AgentConfiguration.AgentConfiguration.DeploymentType - } else if systemAgentConfig != nil && systemAgentConfig.Package.Container.Image != "" { - // If container image is specified, use container - deploymentType = deploymentTypeContainer - } else { - // Default to container if DeploymentType is nil - deploymentType = deploymentTypeContainer - } - - // Get agent configuration - use provided config or defaults - var deployAgentConfig rsc.AgentConfiguration - if systemAgentConfig != nil && systemAgentConfig.AgentConfiguration != nil { - // Use provided configuration - deployAgentConfig = *systemAgentConfig.AgentConfiguration - // Ensure host is set - if deployAgentConfig.Host == nil { - deployAgentConfig.Host = &ctrl.Host - } - // Ensure IsSystem is always true for system agents - deployAgentConfig.IsSystem = iutil.MakeBoolPtr(true) - // Ensure DeploymentType is set (default to container if nil) - if deployAgentConfig.AgentConfiguration.DeploymentType == nil { - deployAgentConfig.AgentConfiguration.DeploymentType = iutil.MakeStrPtr(deploymentType) - } - } else { - // Use defaults with configurable ports (router mode always interior) - RouterConfig := client.RouterConfig{ - RouterMode: iutil.MakeStrPtr(iofog.RouterModeInterior), - MessagingPort: iutil.MakeIntPtr(5671), - EdgeRouterPort: iutil.MakeIntPtr(45671), - InterRouterPort: iutil.MakeIntPtr(55671), - } - - upstreamRouters := []string{} - upstreamNatsServers := []string{} - - deployAgentConfig = rsc.AgentConfiguration{ - Name: ctrl.Name, - FogType: iutil.MakeStrPtr("auto"), - AgentConfiguration: client.AgentConfiguration{ - IsSystem: iutil.MakeBoolPtr(true), - DeploymentType: iutil.MakeStrPtr(deploymentType), - Host: &ctrl.Host, - RouterConfig: RouterConfig, - UpstreamRouters: &upstreamRouters, - UpstreamNatsServers: &upstreamNatsServers, - }, - } - } - - // Ensure router mode is always "interior" for system agents - if deployAgentConfig.RouterConfig.RouterMode == nil { - interior := iofog.RouterModeInterior - deployAgentConfig.RouterConfig.RouterMode = &interior - } else if *deployAgentConfig.RouterConfig.RouterMode != iofog.RouterModeInterior { - // Force to interior mode - interior := iofog.RouterModeInterior - deployAgentConfig.RouterConfig.RouterMode = &interior - } - - if deployAgentConfig.RouterConfig.EdgeRouterPort == nil { - edgeRouterPort := 45671 - deployAgentConfig.RouterConfig.EdgeRouterPort = &edgeRouterPort - } - if deployAgentConfig.RouterConfig.InterRouterPort == nil { - interRouterPort := 55671 - deployAgentConfig.RouterConfig.InterRouterPort = &interRouterPort - } - - if deployAgentConfig.RouterConfig.MessagingPort == nil { - messagingPort := 5671 - deployAgentConfig.RouterConfig.MessagingPort = &messagingPort - } - - // System agents run NATS in server mode (like router interior). Apply default natsConfig when not provided. - applySystemAgentNatsDefaults(&deployAgentConfig) - - // Ensure name is set - if deployAgentConfig.Name == "" { - deployAgentConfig.Name = ctrl.Name - } - - agent := rsc.RemoteAgent{ - Name: ctrl.Name, - Host: ctrl.Host, - SSH: ctrl.SSH, - Config: &deployAgentConfig, - } - // Set Package and Scripts if systemAgentConfig is provided - if systemAgentConfig != nil { - agent.Package = systemAgentConfig.Package - agent.Scripts = systemAgentConfig.Scripts // Support custom scripts - } - - // Get Agentconfig executor - deployAgentConfigExecutor := deployagentconfig.NewRemoteExecutor(ctrl.Name, &deployAgentConfig, namespace, nil) - // If there already is a system fog, ignore error - if err := deployAgentConfigExecutor.Execute(); err != nil { - return err - } - agent.UUID = deployAgentConfigExecutor.GetAgentUUID() - agentDeployExecutor, err := deployagent.NewRemoteExecutor(namespace, &agent, true) // isSystem = true - if err != nil { - return err - } - return agentDeployExecutor.Execute() +type hostExecutor struct { + namespace string + name string + cp *rsc.RemoteControlPlane + ctrl *rsc.RemoteController } -func deployNextSystemAgent(namespace string, ctrl *rsc.RemoteController, systemAgentConfig *rsc.SystemAgentConfig) (err error) { - // Deploy system agent to host internal router - install.Verbose("Deploying next-system agent for controller " + ctrl.Name) - // If DeploymentType is nil, default to "container" - var deploymentType string - if systemAgentConfig != nil && systemAgentConfig.AgentConfiguration != nil && systemAgentConfig.AgentConfiguration.AgentConfiguration.DeploymentType != nil { - // Use DeploymentType from provided configuration - deploymentType = *systemAgentConfig.AgentConfiguration.AgentConfiguration.DeploymentType - } else if systemAgentConfig != nil && systemAgentConfig.Package.Container.Image != "" { - // If container image is specified, use container - deploymentType = deploymentTypeContainer - } else { - // Default to container if DeploymentType is nil - deploymentType = deploymentTypeContainer - } - - // Get agent configuration - use provided config or defaults - var deployAgentConfig rsc.AgentConfiguration - if systemAgentConfig != nil && systemAgentConfig.AgentConfiguration != nil { - // Use provided configuration - deployAgentConfig = *systemAgentConfig.AgentConfiguration - // Ensure host is set - if deployAgentConfig.Host == nil { - deployAgentConfig.Host = &ctrl.Host - } - // Ensure IsSystem is always true for system agents - deployAgentConfig.IsSystem = iutil.MakeBoolPtr(true) - // Ensure DeploymentType is set (default to container if nil) - if deployAgentConfig.AgentConfiguration.DeploymentType == nil { - deployAgentConfig.AgentConfiguration.DeploymentType = iutil.MakeStrPtr(deploymentType) - } - // Override upstream routers for non-first controllers - if deployAgentConfig.UpstreamRouters == nil { - upstreamRouters := []string{"default-router"} - deployAgentConfig.UpstreamRouters = &upstreamRouters - } else { - // Add default-router if not already present - hasDefaultRouter := false - for _, router := range *deployAgentConfig.UpstreamRouters { - if router == "default-router" { - hasDefaultRouter = true - break - } - } - if !hasDefaultRouter { - *deployAgentConfig.UpstreamRouters = append(*deployAgentConfig.UpstreamRouters, "default-router") - } - } - // Override upstream nats server for non-first controllers - if deployAgentConfig.UpstreamNatsServers == nil { - upstreamNatsServers := []string{"default-nats-hub"} - deployAgentConfig.UpstreamNatsServers = &upstreamNatsServers - } else { - // Add default-nats-hub if not already present - hasDefaultNatsHub := false - for _, natsServer := range *deployAgentConfig.UpstreamNatsServers { - if natsServer == "default-nats-hub" { - hasDefaultNatsHub = true - break - } - } - if !hasDefaultNatsHub { - *deployAgentConfig.UpstreamNatsServers = append(*deployAgentConfig.UpstreamNatsServers, "default-nats-hub") - } - } - } else { - // Use defaults with configurable ports (router mode always interior) - RouterConfig := client.RouterConfig{ - RouterMode: iutil.MakeStrPtr(iofog.RouterModeInterior), - MessagingPort: iutil.MakeIntPtr(5671), - EdgeRouterPort: iutil.MakeIntPtr(45671), - InterRouterPort: iutil.MakeIntPtr(55671), - } - - upstreamRouters := []string{"default-router"} - - deployAgentConfig = rsc.AgentConfiguration{ - Name: ctrl.Name, - FogType: iutil.MakeStrPtr("auto"), - AgentConfiguration: client.AgentConfiguration{ - IsSystem: iutil.MakeBoolPtr(true), - DeploymentType: iutil.MakeStrPtr(deploymentType), - Host: &ctrl.Host, - RouterConfig: RouterConfig, - UpstreamRouters: &upstreamRouters, - }, - } - } - - // Ensure router mode is always "interior" for system agents - if deployAgentConfig.RouterConfig.RouterMode == nil { - interior := iofog.RouterModeInterior - deployAgentConfig.RouterConfig.RouterMode = &interior - } else if *deployAgentConfig.RouterConfig.RouterMode != iofog.RouterModeInterior { - // Force to interior mode - interior := iofog.RouterModeInterior - deployAgentConfig.RouterConfig.RouterMode = &interior - } - if deployAgentConfig.RouterConfig.EdgeRouterPort == nil { - edgeRouterPort := 45671 - deployAgentConfig.RouterConfig.EdgeRouterPort = &edgeRouterPort - } - if deployAgentConfig.RouterConfig.InterRouterPort == nil { - interRouterPort := 55671 - deployAgentConfig.RouterConfig.InterRouterPort = &interRouterPort - } - - if deployAgentConfig.RouterConfig.MessagingPort == nil { - messagingPort := 5671 - deployAgentConfig.RouterConfig.MessagingPort = &messagingPort - } - - // System agents run NATS in server mode (like router interior). Apply default natsConfig when not provided. - applySystemAgentNatsDefaults(&deployAgentConfig) - - // Ensure name is set - if deployAgentConfig.Name == "" { - deployAgentConfig.Name = ctrl.Name - } - - agent := rsc.RemoteAgent{ - Name: ctrl.Name, - Host: ctrl.Host, - SSH: ctrl.SSH, - Config: &deployAgentConfig, - } - // Set Package and Scripts if systemAgentConfig is provided - if systemAgentConfig != nil { - agent.Package = systemAgentConfig.Package - agent.Scripts = systemAgentConfig.Scripts // Support custom scripts - } - // Set airgap flag from control plane (get it from namespace) - ns, err := config.GetNamespace(namespace) - if err == nil { - if cp, err := ns.GetControlPlane(); err == nil { - if remoteCP, ok := cp.(*rsc.RemoteControlPlane); ok { - agent.Airgap = remoteCP.Airgap - } - } - } - - // Get Agentconfig executor - deployAgentConfigExecutor := deployagentconfig.NewRemoteExecutor(ctrl.Name, &deployAgentConfig, namespace, nil) - // If there already is a system fog, ignore error - if err := deployAgentConfigExecutor.Execute(); err != nil { - return err - } - agent.UUID = deployAgentConfigExecutor.GetAgentUUID() - agentDeployExecutor, err := deployagent.NewRemoteExecutor(namespace, &agent, true) // isSystem = true - if err != nil { - return err - } - return agentDeployExecutor.Execute() -} - -// prepareViewerURL prepares the viewer URL from controller configuration or endpoint -func prepareViewerURL(endpoint string) (string, error) { - - // Otherwise, construct from endpoint using logic similar to view.go - URL, err := url.Parse(endpoint) - if err != nil || URL.Host == "" { - URL, err = url.Parse("//" + endpoint) - if err != nil { - return "", fmt.Errorf("failed to parse endpoint: %v", err) - } - } - - if URL.Scheme == "" { - URL.Scheme = "http" - } - - host := "" - if strings.Contains(URL.Host, ":") { - host, _, err = net.SplitHostPort(URL.Host) - if err != nil { - return "", fmt.Errorf("failed to split host and port: %v", err) - } - } else { - host = URL.Host - } - - // Add port for localhost - if util.IsLocalHost(host) { - host = net.JoinHostPort(host, iofog.ControllerHostECNViewerPortString) - } - - URL.Host = host - return URL.String(), nil -} - -// updateViewerClientRootURL updates the viewer client root URL in Keycloak if auth is configured -func updateViewerClientRootURL(controlPlane *rsc.RemoteControlPlane, endpoint string) error { - // Check if auth is configured - validate all required fields - auth := controlPlane.Auth - if auth.URL == "" || auth.Realm == "" || auth.ControllerClient == "" || auth.ControllerSecret == "" || auth.ViewerClient == "" { - // Auth not fully configured, skip update - return nil - } - - // Get first controller to check for EcnViewerURL - controllers := controlPlane.GetControllers() - if len(controllers) == 0 { - return fmt.Errorf("no controllers found in control plane") - } - - // Prepare viewer URL - viewerURL, err := prepareViewerURL(endpoint) - if err != nil { - return fmt.Errorf("failed to prepare viewer URL: %v", err) - } - - // Update viewer client root URL - if err := iutil.UpdateECNViewerClientRootURL(controlPlane.Auth, viewerURL); err != nil { - return fmt.Errorf("failed to update viewer client root URL: %v", err) - } - - return nil +func (exe hostExecutor) Execute() error { + return deployRemoteControlPlaneHost(exe.namespace, exe.name, exe.cp, exe.ctrl) } -func tagControllerImage(ctrl *rsc.RemoteController, image string) (err error) { - - if image == "" { - image = util.GetControllerImage() - } - - // Connect - ssh, err := util.NewSecureShellClient(ctrl.SSH.User, ctrl.Host, ctrl.SSH.KeyFile) - if err != nil { - return err - } - if err := ssh.Connect(); err != nil { - return err - } - - defer util.Log(ssh.Disconnect) - - cmds := []string{ - fmt.Sprintf(`echo "IOFOG_CONTROLLER_IMAGE=%s" | sudo tee -a "/etc/iofog/agent/iofog-agent.env" > /dev/null`, image), - fmt.Sprintf("sudo service iofog-agent restart"), - } - - // Execute commands - for _, cmd := range cmds { - _, err = ssh.Run(cmd) - if err != nil { - return - } - } - - return +func (exe hostExecutor) GetName() string { + return exe.ctrl.Name } -func (exe remoteControlPlaneExecutor) postDeploy() (err error) { - controllers := exe.controlPlane.GetControllers() - remoteControlPlane, ok := exe.controlPlane.(*rsc.RemoteControlPlane) - if !ok { - return util.NewInternalError("Could not convert ControlPlane to Remote ControlPlane") - } +func (exe remoteControlPlaneExecutor) Execute() (err error) { + util.SpinStart(fmt.Sprintf("Deploying controlplane %s", exe.GetName())) - // Check if airgap is enabled for system agents - if remoteControlPlane.Airgap { - // Transfer images for system agents before deployment - if err := exe.transferSystemAgentImages(); err != nil { - return fmt.Errorf("failed to transfer airgap images for system agents: %w", err) + if ca := exe.controlPlane.GetTrustCA(); ca != "" { + if err := trust.StoreCA(exe.namespace, ca); err != nil { + return err } } - // Deploy agents for each controller - for idx, baseController := range controllers { - controller, ok := baseController.(*rsc.RemoteController) - if !ok { - return util.NewInternalError("Could not convert Controller to Remote Controller") - } - - // // System agent config is required per controller - // if controller.SystemAgent == nil { - // return fmt.Errorf("controller '%s' must have a systemAgent configuration", controller.Name) - // } - - // First controller gets system agent(with default-router), others get next-system agents(with interior mode) - if idx == 0 { - if err := deploySystemAgent(exe.ns.Name, controller, controller.SystemAgent); err != nil { - return fmt.Errorf("failed to deploy system agent for first controller: %v", err) - } - } else { - if err := deployNextSystemAgent(exe.ns.Name, controller, controller.SystemAgent); err != nil { - return fmt.Errorf("failed to deploy next-system agent for controller %d: %v", idx, err) - } - } - var image string - // Check if controller has custom install script args (highest priority) - if controller.Scripts != nil && controller.Scripts.Install.Args != nil && len(controller.Scripts.Install.Args) > 0 { - image = controller.Scripts.Install.Args[0] - } else if remoteControlPlane.Package.Container.Image != "" { - // Use image from control plane package - image = remoteControlPlane.Package.Container.Image - } else { - // Default to standard controller image - image = util.GetControllerImage() - } - // Tag controller image for all controllers - if err := tagControllerImage(controller, image); err != nil { - return fmt.Errorf("failed to tag controller image for controller %d: %v", idx, err) + if exe.controlPlane.Airgap { + if err := deployairgap.ValidateControlPlaneAirgapRequirements(exe.controlPlane); err != nil { + return err } } - return nil -} -func (exe remoteControlPlaneExecutor) Execute() (err error) { - util.SpinStart(fmt.Sprintf("Deploying controlplane %s", exe.GetName())) - - // Check if airgap is enabled - remoteControlPlane, ok := exe.controlPlane.(*rsc.RemoteControlPlane) - if ok && remoteControlPlane.Airgap { - if err := deployairgap.ValidateControlPlaneAirgapRequirements(remoteControlPlane); err != nil { - return err - } - // Transfer only controller image; router and debugger are transferred in system agent phase - if err := exe.transferControllerImages(); err != nil { - return fmt.Errorf("failed to transfer airgap images for controllers: %w", err) + hostExecutors := make([]execute.Executor, len(exe.controlPlane.Controllers)) + for idx := range exe.controlPlane.Controllers { + ctrl := &exe.controlPlane.Controllers[idx] + hostExecutors[idx] = hostExecutor{ + namespace: exe.namespace, + name: exe.name, + cp: exe.controlPlane, + ctrl: ctrl, } } - - if err := runExecutors(exe.controllerExecutors); err != nil { + if err := runExecutors(hostExecutors); err != nil { return err } - // Make sure Controller API is ready endpoint, err := exe.controlPlane.GetEndpoint() if err != nil { - return + return err } - if err := install.WaitForControllerAPI(endpoint); err != nil { + + ns, err := config.GetNamespace(exe.namespace) + if err != nil { return err } - // // Create new user - // baseURL, err := util.GetBaseURL(endpoint) - // if err != nil { - // return err - // } - // exe.ctrlClient = client.New(client.Options{BaseURL: baseURL}) - // user := client.User(exe.controlPlane.GetUser()) - // user.Password = exe.controlPlane.GetUser().GetRawPassword() - // if err = exe.ctrlClient.CreateUser(user); err != nil { - // // If not error about account existing, fail - // if !strings.Contains(err.Error(), "already an account associated") { - // return err - // } - // // Try to log in - // if err := exe.ctrlClient.Login(client.LoginRequest{ - // Email: user.Email, - // Password: user.Password, - // }); err != nil { - // return err - // } - // } - // Update config - exe.ns.SetControlPlane(exe.controlPlane) + ns.SetControlPlane(exe.controlPlane) if err := config.Flush(); err != nil { return err } - // Update viewer client root URL if auth is configured - if ok { - if err := updateViewerClientRootURL(remoteControlPlane, endpoint); err != nil { - // Log error but don't fail deployment - util.PrintInfo(fmt.Sprintf("Warning: Failed to update viewer client root URL: %v\n", err)) - } - } - - // Post deploy steps - return exe.postDeploy() -} - -func (exe remoteControlPlaneExecutor) GetName() string { - return exe.name -} - -func newControlPlaneExecutor(executors []execute.Executor, namespace *rsc.Namespace, name string, controlPlane rsc.ControlPlane) execute.Executor { - return remoteControlPlaneExecutor{ - controllerExecutors: executors, - ns: namespace, - controlPlane: controlPlane, - name: name, - } -} - -// Validates database configuration for multi-controller setup -func validateMultiControllerDatabase(controlPlane *rsc.RemoteControlPlane) error { - if len(controlPlane.Controllers) > 1 { - db := controlPlane.Database - if db.Provider == "" || db.Host == "" || db.DatabaseName == "" || - db.Password == "" || db.Port == 0 || db.User == "" { - return util.NewInputError("When deploying multiple controllers, you must specify an external database configuration with all required fields (host, user, password, provider, databaseName, port)") - } - } - return nil -} - -// Validates HTTPS configuration for a single controller -func validateControllerHTTPS(controller *rsc.RemoteController) error { - if controller.Https != nil && controller.Https.Enabled != nil && *controller.Https.Enabled { - // HTTPS is enabled, validate required fields - if controller.Https.TLSCert == "" || controller.Https.TLSKey == "" { - return util.NewInputError("When HTTPS is enabled, you must provide TLS certificate and key") - } - } - return nil -} - -// Validates CA configuration for a controller -func validateControllerRouterCA(controller *rsc.RemoteController) error { - if controller.SiteCA != nil { - if controller.SiteCA.TLSCert == "" || controller.SiteCA.TLSKey == "" { - return util.NewInputError("When SiteCA is configured, you must provide both TLS certificate and key") - } - } - if controller.LocalCA != nil { - if controller.LocalCA.TLSCert == "" || controller.LocalCA.TLSKey == "" { - return util.NewInputError("When LocalCA is configured, you must provide both TLS certificate and key") - } - } - return nil -} - -// Validates HTTPS configuration across all controllers -func validateMultiControllerHTTPS(controlPlane *rsc.RemoteControlPlane) error { - controllers := controlPlane.Controllers - if len(controllers) <= 1 { - return nil - } - - // Check first controller's HTTPS config - firstController := controllers[0] - if firstController.Https != nil && firstController.Https.Enabled != nil && *firstController.Https.Enabled { - // First controller has HTTPS enabled, validate all controllers - for idx, controller := range controllers { - if err := validateControllerHTTPS(&controller); err != nil { - return fmt.Errorf("controller %d (%s): %v", idx, controller.Name, err) - } - } + if err := trust.WaitForControllerAPI(context.Background(), exe.namespace, endpoint); err != nil { + return err } - return nil -} -// Validates CA configuration across all controllers -func validateMultiControllerRouterCA(controlPlane *rsc.RemoteControlPlane) error { - controllers := controlPlane.Controllers - if len(controllers) <= 1 { - return nil + if err := auth.EnsureIofogUserEmbedded(context.Background(), exe.namespace, endpoint, auth.EmbeddedAuthSpec{ + Mode: exe.controlPlane.Auth.Mode, + Bootstrap: exe.controlPlane.Auth.Bootstrap, + User: &exe.controlPlane.IofogUser, + }); err != nil { + return err } - // Only first controller should have CA configuration - firstController := controllers[0] - if firstController.SiteCA != nil || firstController.LocalCA != nil { - // Validate first controller's CA config - if err := validateControllerRouterCA(&firstController); err != nil { - return fmt.Errorf("first controller (%s): %v", firstController.Name, err) - } - - // Check that other controllers don't have CA config - for idx, controller := range controllers[1:] { - if controller.SiteCA != nil || controller.LocalCA != nil { - return fmt.Errorf("controller %d (%s): CA configuration should only be specified for the first controller", idx+1, controller.Name) - } - } + if err := deployControllerRegistry(exe.namespace, exe.controlPlane); err != nil { + return err } - return nil -} - -// // Validates that each controller has a systemAgent configuration -// func validateControllerSystemAgent(controlPlane *rsc.RemoteControlPlane) error { -// controllers := controlPlane.Controllers -// if len(controllers) == 0 { -// return util.NewInputError("Remote Control Plane must have at least one controller") -// } - -// for idx, controller := range controllers { -// if controller.SystemAgent == nil { -// return fmt.Errorf("controller %d (%s): systemAgent configuration is required", idx, controller.Name) -// } -// // Validate systemAgent package is provided -// if controller.SystemAgent.Package.Container.Image == "" && controller.SystemAgent.Package.Version == "" && controller.SystemAgent.Scripts.Install.Args == nil { -// return fmt.Errorf("controller %d (%s): systemAgent must have either package.container.image or package.version or scripts.install.args specified", idx, controller.Name) -// } -// } -// return nil -// } -// Main validation function that orchestrates all validations -func validateMultiControllerConfig(controlPlane *rsc.RemoteControlPlane) error { - // // Validate systemAgent configuration - // if err := validateControllerSystemAgent(controlPlane); err != nil { - // return err - // } - - // Validate database configuration - if err := validateMultiControllerDatabase(controlPlane); err != nil { + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { return err } - - // Validate HTTPS configuration - if err := validateMultiControllerHTTPS(controlPlane); err != nil { + if err := install.DeployGlobalCertificates(clt, globalCertificatesFromCP(exe.controlPlane)); err != nil { return err } - // Validate CA configuration - if err := validateMultiControllerRouterCA(controlPlane); err != nil { + if err := deployRemoteSystemAgents(exe.namespace, exe.controlPlane); err != nil { return err } - // Validate Vault when set (provider and required provider fields) - if err := validateRemoteVault(controlPlane); err != nil { + if err := rsc.BackfillConsoleURL(exe.controlPlane); err != nil { return err } - return nil + ns.SetControlPlane(exe.controlPlane) + return config.Flush() } -func validateRemoteVault(controlPlane *rsc.RemoteControlPlane) error { - if controlPlane.Vault == nil { - return nil - } - v := controlPlane.Vault - if v.Provider == "" { - return nil - } - switch v.Provider { - case "hashicorp", "openbao", "vault": - if v.Hashicorp == nil || (v.Hashicorp.Address == "" && v.Hashicorp.Token == "") { - return util.NewInputError("Vault provider " + v.Provider + " requires hashicorp block with address and token") - } - case "aws", "aws-secrets-manager": - if v.Aws == nil { - return util.NewInputError("Vault provider " + v.Provider + " requires aws block") - } - case "azure", "azure-key-vault": - if v.Azure == nil { - return util.NewInputError("Vault provider " + v.Provider + " requires azure block") - } - case "google", "google-secret-manager": - if v.Google == nil { - return util.NewInputError("Vault provider " + v.Provider + " requires google block") - } - } - return nil +func (exe remoteControlPlaneExecutor) GetName() string { + return exe.name } -// transferControllerImages transfers only the controller image for airgap deployment. -// Router and debugger are transferred in the system agent phase (transferSystemAgentImages). -func (exe remoteControlPlaneExecutor) transferControllerImages() error { - remoteControlPlane, ok := exe.controlPlane.(*rsc.RemoteControlPlane) - if !ok { - return util.NewInternalError("Could not convert ControlPlane to Remote ControlPlane") - } - - isInitial, err := deployairgap.IsInitialDeployment(exe.ns.Name) +func NewExecutor(opt Options) (exe execute.Executor, err error) { + _, err = config.GetNamespace(opt.Namespace) if err != nil { - return fmt.Errorf("failed to determine deployment type: %w", err) + return } - images, err := deployairgap.CollectControllerImages(exe.ns.Name, remoteControlPlane, isInitial) + controlPlane, err := rsc.UnmarshallRemoteControlPlane(opt.Yaml) if err != nil { - return fmt.Errorf("failed to collect controller images: %w", err) - } - - // Transfer controller and NATS images (remote Controller runs/starts NATS). - // Use platform and container engine from system agent config (validated when airgap is enabled). - imageList := []string{images.Controller} - if images.Nats != "" { - imageList = append(imageList, images.Nats) + return } - controllers := remoteControlPlane.GetControllers() - ctx := context.Background() - for _, baseController := range controllers { - controller, ok := baseController.(*rsc.RemoteController) - if !ok { - return util.NewInternalError("Could not convert Controller to Remote Controller") - } - platform, err := deployairgap.ResolvePlatform(controller.SystemAgent.AgentConfiguration.FogType) - if err != nil { - return fmt.Errorf("controller %s: %w", controller.Name, err) - } - engine, err := deployairgap.ResolveContainerEngine(controller.SystemAgent.AgentConfiguration.AgentConfiguration.ContainerEngine) - if err != nil { - return fmt.Errorf("controller %s: %w", controller.Name, err) - } - if err := deployairgap.TransferAirgapImages(ctx, exe.ns.Name, controller.Host, &controller.SSH, platform, engine, imageList); err != nil { - return fmt.Errorf("failed to transfer images to controller %s: %w", controller.Name, err) - } - } + applySystemMicroserviceDefaults(&controlPlane) - return nil + return remoteControlPlaneExecutor{ + controlPlane: &controlPlane, + namespace: opt.Namespace, + name: opt.Name, + }, nil } -// transferSystemAgentImages transfers agent, router, and debugger images for system agents in airgap deployment -func (exe remoteControlPlaneExecutor) transferSystemAgentImages() error { - remoteControlPlane, ok := exe.controlPlane.(*rsc.RemoteControlPlane) - if !ok { - return util.NewInternalError("Could not convert ControlPlane to Remote ControlPlane") +func applySystemMicroserviceDefaults(cp *rsc.RemoteControlPlane) { + if cp.SystemMicroservices.Router.AMD64 == "" { + cp.SystemMicroservices.Router.AMD64 = util.GetRouterImage() } - - // Determine if this is initial deployment - isInitial, err := deployairgap.IsInitialDeployment(exe.ns.Name) - if err != nil { - return fmt.Errorf("failed to determine deployment type: %w", err) + if cp.SystemMicroservices.Router.ARM64 == "" { + cp.SystemMicroservices.Router.ARM64 = util.GetRouterImage() } - - controllers := remoteControlPlane.GetControllers() - for _, baseController := range controllers { - controller, ok := baseController.(*rsc.RemoteController) - if !ok { - return util.NewInternalError("Could not convert Controller to Remote Controller") - } - - // Skip if no system agent config - if controller.SystemAgent == nil || controller.SystemAgent.AgentConfiguration == nil { - continue - } - - // Validate airgap requirements for system agent - if err := deployairgap.ValidateAirgapRequirements(controller.SystemAgent.AgentConfiguration); err != nil { - return fmt.Errorf("system agent for controller %s: %w", controller.Name, err) - } - - // Resolve platform and container engine - platform, err := deployairgap.ResolvePlatform(controller.SystemAgent.AgentConfiguration.FogType) - if err != nil { - return fmt.Errorf("system agent for controller %s: %w", controller.Name, err) - } - - engine, err := deployairgap.ResolveContainerEngine(controller.SystemAgent.AgentConfiguration.AgentConfiguration.ContainerEngine) - if err != nil { - return fmt.Errorf("system agent for controller %s: %w", controller.Name, err) - } - - // Create a temporary RemoteAgent for image collection - tempAgent := &rsc.RemoteAgent{ - Name: controller.Name, - Host: controller.Host, - SSH: controller.SSH, - Package: controller.SystemAgent.Package, - Config: controller.SystemAgent.AgentConfiguration, - } - - // Collect required images - images, err := deployairgap.CollectAgentImages(exe.ns.Name, tempAgent, remoteControlPlane, isInitial) - if err != nil { - return fmt.Errorf("failed to collect agent images for system agent %s: %w", controller.Name, err) - } - - // Get router image for the platform - routerImage, err := deployairgap.GetImageForPlatform(images, platform) - if err != nil { - return fmt.Errorf("failed to get router image for platform %s: %w", platform, err) - } - - // Prepare image list (agent, router for platform, NATS, debugger if available) - imageList := []string{images.Agent} - if routerImage != "" { - imageList = append(imageList, routerImage) - } - if images.Nats != "" { - imageList = append(imageList, images.Nats) - } - if images.Debugger != "" { - imageList = append(imageList, images.Debugger) - } - - // Transfer images - ctx := context.Background() - if err := deployairgap.TransferAirgapImages(ctx, exe.ns.Name, controller.Host, &controller.SSH, platform, engine, imageList); err != nil { - return fmt.Errorf("failed to transfer images to system agent %s: %w", controller.Name, err) - } + if cp.SystemMicroservices.Router.RISCV64 == "" { + cp.SystemMicroservices.Router.RISCV64 = util.GetRouterImage() } - - return nil -} - -func NewExecutor(opt Options) (exe execute.Executor, err error) { - // Check the namespace exists - ns, err := config.GetNamespace(opt.Namespace) - if err != nil { - return + if cp.SystemMicroservices.Router.ARM == "" { + cp.SystemMicroservices.Router.ARM = util.GetRouterImage() } - - // Read the input file - controlPlane, err := rsc.UnmarshallRemoteControlPlane(opt.Yaml) - if err != nil { - return + if cp.SystemMicroservices.Nats.AMD64 == "" { + cp.SystemMicroservices.Nats.AMD64 = util.GetNatsImage() } - - // Validate control plane for multiple controllers - if err := validateMultiControllerConfig(&controlPlane); err != nil { - return nil, err + if cp.SystemMicroservices.Nats.ARM64 == "" { + cp.SystemMicroservices.Nats.ARM64 = util.GetNatsImage() } - - // Create exe Controllers - controllers := controlPlane.GetControllers() - controllerExecutors := make([]execute.Executor, len(controllers)) - for idx := range controllers { - controller, ok := controllers[idx].(*rsc.RemoteController) - if !ok { - return nil, util.NewError("Could not convert Controller to Remote Controller") - } - exe, err := deployremotecontroller.NewExecutorWithoutParsing(opt.Namespace, &controlPlane, controller) - if err != nil { - return nil, err - } - controllerExecutors[idx] = exe + if cp.SystemMicroservices.Nats.RISCV64 == "" { + cp.SystemMicroservices.Nats.RISCV64 = util.GetNatsImage() } - - return newControlPlaneExecutor(controllerExecutors, ns, opt.Name, &controlPlane), nil -} - -func runExecutors(executors []execute.Executor) error { - if errs, _ := execute.ForParallel(executors); len(errs) > 0 { - return execute.CoalesceErrors(errs) + if cp.SystemMicroservices.Nats.ARM == "" { + cp.SystemMicroservices.Nats.ARM = util.GetNatsImage() } - return nil } diff --git a/internal/deploy/controlplane/remote/manifest.go b/internal/deploy/controlplane/remote/manifest.go new file mode 100644 index 000000000..1fedda29a --- /dev/null +++ b/internal/deploy/controlplane/remote/manifest.go @@ -0,0 +1,185 @@ +package deployremotecontrolplane + +const ( + edgeletAPIVersion = "edgelet.iofog.org/v1" + edgeletControlPlaneKind = "ControlPlane" + edgeletRegistryKind = "Registry" + + // EdgeletRegistryOnline is docker.io (edgelet registry ls ID 1) for online pulls. + EdgeletRegistryOnline = 1 + // EdgeletRegistryAirgap is from_cache (edgelet registry ls ID 2) for pre-loaded images. + EdgeletRegistryAirgap = 2 +) + +// edgeletControlPlaneManifest mirrors edgelet ControlPlane YAML (edgelet.iofog.org/v1). +type edgeletControlPlaneManifest struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata edgeletControlPlaneMetadata `yaml:"metadata"` + Spec edgeletControlPlaneManifestSpec `yaml:"spec"` +} + +type edgeletControlPlaneMetadata struct { + Name string `yaml:"name"` + Namespace string `yaml:"namespace,omitempty"` +} + +type edgeletControlPlaneManifestSpec struct { + Controller edgeletControllerSpec `yaml:"controller"` + Console *edgeletConsoleSpec `yaml:"console,omitempty"` + Database *edgeletDatabaseSpec `yaml:"database,omitempty"` + Auth edgeletAuthSpec `yaml:"auth"` + Events *edgeletEventsSpec `yaml:"events,omitempty"` + SystemMicroservices *edgeletSystemMicroservices `yaml:"systemMicroservices,omitempty"` + Nats *edgeletNatsSpec `yaml:"nats,omitempty"` + LogLevel string `yaml:"logLevel,omitempty"` + TLS *edgeletTLSSpec `yaml:"tls,omitempty"` + Vault *edgeletVaultSpec `yaml:"vault,omitempty"` +} + +type edgeletControllerSpec struct { + Image string `yaml:"image"` + Registry *int `yaml:"registry,omitempty"` + Port *int `yaml:"port,omitempty"` + PublicURL string `yaml:"publicUrl,omitempty"` + TrustProxy *bool `yaml:"trustProxy,omitempty"` +} + +type edgeletConsoleSpec struct { + Port *int `yaml:"port,omitempty"` + URL string `yaml:"url,omitempty"` +} + +type edgeletDatabaseSpec struct { + Provider string `yaml:"provider,omitempty"` + User string `yaml:"user,omitempty"` + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` + Password string `yaml:"password,omitempty"` + DatabaseName string `yaml:"databaseName,omitempty"` + SSL *bool `yaml:"ssl,omitempty"` + CA *string `yaml:"ca,omitempty"` +} + +type edgeletAuthSpec struct { + Mode string `yaml:"mode"` + InsecureAllowHTTP *bool `yaml:"insecureAllowHttp,omitempty"` + InsecureAllowBootstrapLog *bool `yaml:"insecureAllowBootstrapLog,omitempty"` + Bootstrap *edgeletAuthBootstrap `yaml:"bootstrap,omitempty"` + IssuerURL string `yaml:"issuerUrl,omitempty"` + Client *edgeletAuthClient `yaml:"client,omitempty"` + ConsoleClient string `yaml:"consoleClient,omitempty"` + ConsoleClientEnabled *bool `yaml:"consoleClientEnabled,omitempty"` + RateLimit *edgeletAuthRateLimit `yaml:"rateLimit,omitempty"` + SessionStore *edgeletAuthSessionStore `yaml:"sessionStore,omitempty"` + TokenTTL *edgeletAuthTokenTTL `yaml:"tokenTtl,omitempty"` + OIDCTTL *edgeletAuthOIDCTTL `yaml:"oidcTtl,omitempty"` +} + +type edgeletAuthBootstrap struct { + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` +} + +type edgeletAuthClient struct { + ID string `yaml:"id,omitempty"` + Secret string `yaml:"secret,omitempty"` +} + +type edgeletAuthRateLimit struct { + Enabled *bool `yaml:"enabled,omitempty"` + MaxRequestsPerWindow int `yaml:"maxRequestsPerWindow,omitempty"` + WindowMs int `yaml:"windowMs,omitempty"` +} + +type edgeletAuthSessionStore struct { + Type string `yaml:"type,omitempty"` + TTLMs int `yaml:"ttlMs,omitempty"` + Secret string `yaml:"secret,omitempty"` +} + +type edgeletAuthTokenTTL struct { + AccessTokenTTLSeconds int `yaml:"accessTokenTtlSeconds,omitempty"` + RefreshTokenTTLSeconds int `yaml:"refreshTokenTtlSeconds,omitempty"` +} + +type edgeletAuthOIDCTTL struct { + InteractionTTLSeconds int `yaml:"interactionTtlSeconds,omitempty"` + GrantTTLSeconds int `yaml:"grantTtlSeconds,omitempty"` + SessionTTLSeconds int `yaml:"sessionTtlSeconds,omitempty"` + IDTokenTTLSeconds int `yaml:"idTokenTtlSeconds,omitempty"` +} + +type edgeletEventsSpec struct { + AuditEnabled *bool `yaml:"auditEnabled,omitempty"` + RetentionDays int `yaml:"retentionDays,omitempty"` + CleanupInterval int `yaml:"cleanupInterval,omitempty"` + CaptureIPAddress *bool `yaml:"captureIpAddress,omitempty"` +} + +type edgeletSystemMicroservices struct { + Router map[string]string `yaml:"router,omitempty"` + Nats map[string]string `yaml:"nats,omitempty"` +} + +type edgeletNatsSpec struct { + Enabled *bool `yaml:"enabled,omitempty"` +} + +type edgeletTLSBase64 struct { + CA string `yaml:"ca,omitempty"` + Cert string `yaml:"cert,omitempty"` + Key string `yaml:"key,omitempty"` +} + +type edgeletTLSSpec struct { + Base64 *edgeletTLSBase64 `yaml:"base64,omitempty"` +} + +type edgeletVaultSpec struct { + Enabled *bool `yaml:"enabled,omitempty"` + Provider string `yaml:"provider,omitempty"` + BasePath string `yaml:"basePath,omitempty"` + Hashicorp *edgeletVaultHashicorp `yaml:"hashicorp,omitempty"` + Aws *edgeletVaultAws `yaml:"aws,omitempty"` + Azure *edgeletVaultAzure `yaml:"azure,omitempty"` + Google *edgeletVaultGoogle `yaml:"google,omitempty"` +} + +type edgeletVaultHashicorp struct { + Address string `yaml:"address,omitempty"` + Token string `yaml:"token,omitempty"` + Mount string `yaml:"mount,omitempty"` +} + +type edgeletVaultAws struct { + Region string `yaml:"region,omitempty"` + AccessKeyID string `yaml:"accessKeyId,omitempty"` + AccessKey string `yaml:"accessKey,omitempty"` +} + +type edgeletVaultAzure struct { + URL string `yaml:"url,omitempty"` + TenantID string `yaml:"tenantId,omitempty"` + ClientID string `yaml:"clientId,omitempty"` + ClientSecret string `yaml:"clientSecret,omitempty"` +} + +type edgeletVaultGoogle struct { + ProjectID string `yaml:"projectId,omitempty"` + Credentials string `yaml:"credentials,omitempty"` +} + +type edgeletRegistryManifest struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Spec edgeletRegistryManifestSpec `yaml:"spec"` +} + +type edgeletRegistryManifestSpec struct { + URL string `yaml:"url"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + Email string `yaml:"email,omitempty"` + Private bool `yaml:"private"` +} diff --git a/internal/deploy/controlplane/remote/system_agent.go b/internal/deploy/controlplane/remote/system_agent.go new file mode 100644 index 000000000..2316b115d --- /dev/null +++ b/internal/deploy/controlplane/remote/system_agent.go @@ -0,0 +1,249 @@ +package deployremotecontrolplane + +import ( + "context" + "fmt" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + deployagentconfig "github.com/eclipse-iofog/iofogctl/internal/deploy/agentconfig" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + iutil "github.com/eclipse-iofog/iofogctl/internal/util" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const ( + deploymentTypeNative = "native" + deploymentTypeContainer = "container" +) + +func resolveSystemAgentDeploymentType(systemAgent *rsc.SystemAgentConfig) string { + if systemAgent != nil && systemAgent.AgentConfiguration != nil && systemAgent.AgentConfiguration.DeploymentType != nil { + return *systemAgent.AgentConfiguration.DeploymentType + } + if systemAgent != nil && systemAgent.Package.Container.Image != "" { + return deploymentTypeContainer + } + return deploymentTypeNative +} + +func buildFirstSystemAgentConfig(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, systemAgent *rsc.SystemAgentConfig) rsc.AgentConfiguration { + deploymentType := resolveSystemAgentDeploymentType(systemAgent) + + var deployAgentConfig rsc.AgentConfiguration + if systemAgent != nil && systemAgent.AgentConfiguration != nil { + deployAgentConfig = *systemAgent.AgentConfiguration + if deployAgentConfig.Host == nil { + deployAgentConfig.Host = defaultSystemAgentHost(cp, ctrl) + } + if deployAgentConfig.DeploymentType == nil { + deployAgentConfig.DeploymentType = iutil.MakeStrPtr(deploymentType) + } + } else { + host := defaultSystemAgentHost(cp, ctrl) + upstreamRouters := []string{} + upstreamNatsServers := []string{} + deployAgentConfig = rsc.AgentConfiguration{ + Name: ctrl.Name, + Arch: iutil.MakeStrPtr("auto"), + AgentConfiguration: client.AgentConfiguration{ + IsSystem: iutil.MakeBoolPtr(true), + DeploymentType: iutil.MakeStrPtr(deploymentType), + Host: host, + RouterConfig: deployagentconfig.DefaultRouterConfig(), + UpstreamRouters: &upstreamRouters, + UpstreamNatsServers: &upstreamNatsServers, + }, + } + } + + deployAgentConfig.IsSystem = iutil.MakeBoolPtr(true) + if deployAgentConfig.DeploymentType == nil { + deployAgentConfig.DeploymentType = iutil.MakeStrPtr(deploymentType) + } + if deployAgentConfig.UpstreamRouters == nil { + emptyRouters := []string{} + deployAgentConfig.UpstreamRouters = &emptyRouters + } + if deployAgentConfig.UpstreamNatsServers == nil { + emptyNats := []string{} + deployAgentConfig.UpstreamNatsServers = &emptyNats + } + + deployagentconfig.ApplySystemAgentDefaults(&deployAgentConfig) + + if deployAgentConfig.Name == "" { + deployAgentConfig.Name = ctrl.Name + } + return deployAgentConfig +} + +func buildNextSystemAgentConfig(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, systemAgent *rsc.SystemAgentConfig) rsc.AgentConfiguration { + deploymentType := resolveSystemAgentDeploymentType(systemAgent) + + var deployAgentConfig rsc.AgentConfiguration + if systemAgent != nil && systemAgent.AgentConfiguration != nil { + deployAgentConfig = *systemAgent.AgentConfiguration + if deployAgentConfig.Host == nil { + deployAgentConfig.Host = defaultSystemAgentHost(cp, ctrl) + } + if deployAgentConfig.DeploymentType == nil { + deployAgentConfig.DeploymentType = iutil.MakeStrPtr(deploymentType) + } + if deployAgentConfig.UpstreamRouters == nil { + upstreamRouters := []string{"default-router"} + deployAgentConfig.UpstreamRouters = &upstreamRouters + } else if !containsString(*deployAgentConfig.UpstreamRouters, "default-router") { + *deployAgentConfig.UpstreamRouters = append(*deployAgentConfig.UpstreamRouters, "default-router") + } + if deployAgentConfig.UpstreamNatsServers == nil { + upstreamNatsServers := []string{"default-nats-hub"} + deployAgentConfig.UpstreamNatsServers = &upstreamNatsServers + } else if !containsString(*deployAgentConfig.UpstreamNatsServers, "default-nats-hub") { + *deployAgentConfig.UpstreamNatsServers = append(*deployAgentConfig.UpstreamNatsServers, "default-nats-hub") + } + } else { + host := defaultSystemAgentHost(cp, ctrl) + upstreamRouters := []string{"default-router"} + upstreamNatsServers := []string{"default-nats-hub"} + deployAgentConfig = rsc.AgentConfiguration{ + Name: ctrl.Name, + Arch: iutil.MakeStrPtr("auto"), + AgentConfiguration: client.AgentConfiguration{ + IsSystem: iutil.MakeBoolPtr(true), + DeploymentType: iutil.MakeStrPtr(deploymentType), + Host: host, + RouterConfig: deployagentconfig.DefaultRouterConfig(), + UpstreamRouters: &upstreamRouters, + UpstreamNatsServers: &upstreamNatsServers, + }, + } + } + + deployAgentConfig.IsSystem = iutil.MakeBoolPtr(true) + if deployAgentConfig.DeploymentType == nil { + deployAgentConfig.DeploymentType = iutil.MakeStrPtr(deploymentType) + } + + deployagentconfig.ApplySystemAgentDefaults(&deployAgentConfig) + + if deployAgentConfig.Name == "" { + deployAgentConfig.Name = ctrl.Name + } + return deployAgentConfig +} + +func containsString(values []string, target string) bool { + for _, value := range values { + if value == target { + return true + } + } + return false +} + +func defaultSystemAgentHost(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController) *string { + host := resolveControllerAPIHost(cp, ctrl) + if host == "" { + return nil + } + return &host +} + +func deployRemoteSystemAgent(namespace string, cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, deployAgentConfig rsc.AgentConfiguration) error { + configExe := deployagentconfig.NewRemoteExecutor(ctrl.Name, &deployAgentConfig, namespace, nil) + if err := configExe.Execute(); err != nil { + return fmt.Errorf("failed to deploy system agent configuration: %w", err) + } + + agentUUID := configExe.GetAgentUUID() + edgelet, err := BuildRemoteEdgelet(cp, ctrl, agentUUID) + if err != nil { + return err + } + + endpoint, err := ResolveControllerHostEndpoint(cp, ctrl) + if err != nil { + return err + } + + user := install.IofogUser(cp.GetUser()) + user.Password = cp.GetUser().GetRawPassword() + opt, err := clientutil.ControllerClientOptions(context.Background(), namespace, endpoint) + if err != nil { + return err + } + if _, err := edgelet.Configure(endpoint, user, opt); err != nil { + return fmt.Errorf("failed to provision system agent: %w", err) + } + + return persistRemoteSystemAgent(namespace, cp, ctrl, deployAgentConfig, endpoint, agentUUID) +} + +func persistRemoteSystemAgent(namespace string, cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, deployAgentConfig rsc.AgentConfiguration, endpoint, uuid string) error { + ns, err := config.GetNamespace(namespace) + if err != nil { + return err + } + + host := ctrl.Host + if deployAgentConfig.Host != nil && strings.TrimSpace(*deployAgentConfig.Host) != "" { + host = *deployAgentConfig.Host + } + + configCopy := deployAgentConfig + agent := &rsc.RemoteAgent{ + Name: ctrl.Name, + UUID: uuid, + Created: util.NowUTC(), + Host: host, + SSH: ctrl.SSH, + ControllerEndpoint: endpoint, + Airgap: cp.Airgap, + Config: &configCopy, + } + if ctrl.SystemAgent != nil { + agent.Package = ctrl.SystemAgent.Package + agent.Scripts = ctrl.SystemAgent.Scripts + } + + if err := ns.UpdateAgent(agent); err != nil { + return err + } + return config.Flush() +} + +func deploySystemAgent(namespace string, cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController) error { + install.Verbose("Deploying system agent for controller " + ctrl.Name) + cfg := buildFirstSystemAgentConfig(cp, ctrl, ctrl.SystemAgent) + return deployRemoteSystemAgent(namespace, cp, ctrl, cfg) +} + +func DeployNextSystemAgent(namespace string, cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController) error { + install.Verbose("Deploying next-system agent for controller " + ctrl.Name) + cfg := buildNextSystemAgentConfig(cp, ctrl, ctrl.SystemAgent) + return deployRemoteSystemAgent(namespace, cp, ctrl, cfg) +} + +func deployRemoteSystemAgents(namespace string, cp *rsc.RemoteControlPlane) error { + controllers := cp.GetControllers() + for idx, baseController := range controllers { + controller, ok := baseController.(*rsc.RemoteController) + if !ok { + return util.NewInternalError("Could not convert Controller to Remote Controller") + } + if idx == 0 { + if err := deploySystemAgent(namespace, cp, controller); err != nil { + return fmt.Errorf("failed to deploy system agent for first controller: %w", err) + } + continue + } + if err := DeployNextSystemAgent(namespace, cp, controller); err != nil { + return fmt.Errorf("failed to deploy next-system agent for controller %d: %w", idx, err) + } + } + return nil +} diff --git a/internal/deploy/controlplane/remote/system_agent_test.go b/internal/deploy/controlplane/remote/system_agent_test.go new file mode 100644 index 000000000..55df93302 --- /dev/null +++ b/internal/deploy/controlplane/remote/system_agent_test.go @@ -0,0 +1,80 @@ +package deployremotecontrolplane + +import ( + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + iutil "github.com/eclipse-iofog/iofogctl/internal/util" + "github.com/eclipse-iofog/iofogctl/pkg/iofog" + "github.com/stretchr/testify/require" +) + +func TestBuildFirstSystemAgentConfig_DefaultsNative(t *testing.T) { + ctrl := &rsc.RemoteController{Name: "remote-1", Host: "10.0.0.1"} + cfg := buildFirstSystemAgentConfig(nil, ctrl, &rsc.SystemAgentConfig{}) + + require.NotNil(t, cfg.IsSystem) + require.True(t, *cfg.IsSystem) + require.NotNil(t, cfg.DeploymentType) + require.Equal(t, deploymentTypeNative, *cfg.DeploymentType) + require.NotNil(t, cfg.UpstreamRouters) + require.Empty(t, *cfg.UpstreamRouters) + require.NotNil(t, cfg.UpstreamNatsServers) + require.Empty(t, *cfg.UpstreamNatsServers) + require.Equal(t, iofog.RouterModeInterior, *cfg.RouterMode) + require.Equal(t, iofog.NatsModeServer, *cfg.NatsMode) +} + +func TestBuildNextSystemAgentConfig_DefaultUpstream(t *testing.T) { + ctrl := &rsc.RemoteController{Name: "remote-2", Host: "10.0.0.2"} + cfg := buildNextSystemAgentConfig(nil, ctrl, &rsc.SystemAgentConfig{}) + + require.NotNil(t, cfg.UpstreamRouters) + require.Equal(t, []string{"default-router"}, *cfg.UpstreamRouters) + require.NotNil(t, cfg.UpstreamNatsServers) + require.Equal(t, []string{"default-nats-hub"}, *cfg.UpstreamNatsServers) + require.Equal(t, deploymentTypeNative, *cfg.DeploymentType) +} + +func TestBuildNextSystemAgentConfig_AppendsDefaultUpstream(t *testing.T) { + ctrl := &rsc.RemoteController{Name: "remote-2", Host: "10.0.0.2"} + upstreamRouters := []string{"custom-router"} + upstreamNats := []string{"custom-nats"} + sys := &rsc.SystemAgentConfig{ + AgentConfiguration: &rsc.AgentConfiguration{ + AgentConfiguration: client.AgentConfiguration{ + UpstreamRouters: &upstreamRouters, + UpstreamNatsServers: &upstreamNats, + }, + }, + } + + cfg := buildNextSystemAgentConfig(nil, ctrl, sys) + require.Equal(t, []string{"custom-router", "default-router"}, *cfg.UpstreamRouters) + require.Equal(t, []string{"custom-nats", "default-nats-hub"}, *cfg.UpstreamNatsServers) +} + +func TestBuildFirstSystemAgentConfig_ContainerWhenImageSet(t *testing.T) { + ctrl := &rsc.RemoteController{Name: "remote-1", Host: "10.0.0.1"} + sys := &rsc.SystemAgentConfig{ + Package: rsc.Package{ + Container: rsc.RemoteContainer{Image: "ghcr.io/example/edgelet:1.0"}, + }, + } + cfg := buildFirstSystemAgentConfig(nil, ctrl, sys) + require.Equal(t, deploymentTypeContainer, *cfg.DeploymentType) +} + +func TestBuildFirstSystemAgentConfig_ForcesSystem(t *testing.T) { + ctrl := &rsc.RemoteController{Name: "remote-1", Host: "10.0.0.1"} + sys := &rsc.SystemAgentConfig{ + AgentConfiguration: &rsc.AgentConfiguration{ + AgentConfiguration: client.AgentConfiguration{ + IsSystem: iutil.MakeBoolPtr(false), + }, + }, + } + cfg := buildFirstSystemAgentConfig(nil, ctrl, sys) + require.True(t, *cfg.IsSystem) +} diff --git a/internal/deploy/controlplane/remote/testdata/edgelet-cp-datasance.yaml b/internal/deploy/controlplane/remote/testdata/edgelet-cp-datasance.yaml new file mode 100644 index 000000000..f67c4aa27 --- /dev/null +++ b/internal/deploy/controlplane/remote/testdata/edgelet-cp-datasance.yaml @@ -0,0 +1,44 @@ +apiVersion: edgelet.iofog.org/v1 +kind: ControlPlane +metadata: + name: iofog + namespace: test-ns +spec: + controller: + image: ghcr.io/datasance/controller:3.8.0-rc.1 + registry: 1 + publicUrl: https://controller.example.com + console: + url: https://controller.example.com + auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + database: + provider: postgres + user: dbuser + host: db.example.com + port: 5432 + password: secret + databaseName: iofog + events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true + systemMicroservices: + router: + amd64: ghcr.io/datasance/router:3.8.0-rc.1 + arm64: ghcr.io/datasance/router:3.8.0-rc.1 + riscv64: ghcr.io/datasance/router:3.8.0-rc.1 + arm: ghcr.io/datasance/router:3.8.0-rc.1 + nats: + amd64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm64: ghcr.io/datasance/nats:2.14.2-rc.2 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm: ghcr.io/datasance/nats:2.14.2-rc.2 + nats: + enabled: true + logLevel: info diff --git a/internal/deploy/controlplane/remote/testdata/edgelet-cp-iofog.yaml b/internal/deploy/controlplane/remote/testdata/edgelet-cp-iofog.yaml new file mode 100644 index 000000000..98608c10d --- /dev/null +++ b/internal/deploy/controlplane/remote/testdata/edgelet-cp-iofog.yaml @@ -0,0 +1,44 @@ +apiVersion: edgelet.iofog.org/v1 +kind: ControlPlane +metadata: + name: iofog + namespace: test-ns +spec: + controller: + image: ghcr.io/eclipse-iofog/controller:3.8.0-rc.1 + registry: 1 + publicUrl: https://controller.example.com + console: + url: https://controller.example.com + auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + database: + provider: postgres + user: dbuser + host: db.example.com + port: 5432 + password: secret + databaseName: iofog + events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true + systemMicroservices: + router: + amd64: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + arm64: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + riscv64: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + arm: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + nats: + amd64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + arm64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + riscv64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + arm: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + nats: + enabled: true + logLevel: info diff --git a/internal/deploy/controlplane/remote/translate.go b/internal/deploy/controlplane/remote/translate.go new file mode 100644 index 000000000..543fa6a5c --- /dev/null +++ b/internal/deploy/controlplane/remote/translate.go @@ -0,0 +1,404 @@ +package deployremotecontrolplane + +import ( + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" +) + +// TranslateOptions configures RemoteControlPlane → edgelet manifest translation. +type TranslateOptions struct { + Name string + Namespace string + ControllerImage string + RouterImage string + NatsImage string + RegistryID *int +} + +// TranslateResult holds optional edgelet Registry YAML and required ControlPlane YAML. +type TranslateResult struct { + Registry []byte + ControlPlane []byte +} + +type translateOptions struct { + name string + namespace string + controllerImage string + routerImage string + natsImage string + registryID *int +} + +func defaultTranslateOptions(name, namespace string) translateOptions { + return translateOptions{ + name: name, + namespace: namespace, + controllerImage: util.GetControllerImage(), + routerImage: util.GetRouterImage(), + natsImage: util.GetNatsImage(), + } +} + +func (o TranslateOptions) mergeDefaults(namespace string) translateOptions { + base := defaultTranslateOptions(o.Name, namespace) + if o.Name != "" { + base.name = o.Name + } + if o.Namespace != "" { + base.namespace = o.Namespace + } + if o.ControllerImage != "" { + base.controllerImage = o.ControllerImage + } + if o.RouterImage != "" { + base.routerImage = o.RouterImage + } + if o.NatsImage != "" { + base.natsImage = o.NatsImage + } + base.registryID = o.RegistryID + return base +} + +// NeedsPrivateEdgeletRegistry reports whether controller.package requires an edgelet Registry manifest. +func NeedsPrivateEdgeletRegistry(cp *rsc.RemoteControlPlane) bool { + pkg := cp.Controller.Package + if pkg == nil { + return false + } + return pkg.Registry != "" && pkg.Username != "" && pkg.Password != "" +} + +// ResolveEdgeletRegistryID picks spec.controller.registry for the edgelet ControlPlane manifest. +func ResolveEdgeletRegistryID(cp *rsc.RemoteControlPlane, privateRegistryID *int) *int { + if privateRegistryID != nil { + return privateRegistryID + } + id := EdgeletRegistryOnline + if cp != nil && cp.Airgap { + id = EdgeletRegistryAirgap + } + return &id +} + +// EffectiveControllerTLS returns per-controller tls override or global spec.tls. +func EffectiveControllerTLS(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController) *rsc.ControlPlaneTLS { + if ctrl != nil && ctrl.TLS != nil { + return ctrl.TLS + } + if cp != nil { + return cp.TLS + } + return nil +} + +// TranslateRemoteControlPlane maps global RemoteControlPlane + per-host controller to edgelet YAML. +func TranslateRemoteControlPlane(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, opts TranslateOptions) (TranslateResult, error) { + tOpts := opts.mergeDefaults(opts.Namespace) + var out TranslateResult + + if NeedsPrivateEdgeletRegistry(cp) { + reg, err := translateEdgeletRegistry(cp) + if err != nil { + return out, err + } + data, err := yaml.Marshal(reg) + if err != nil { + return out, err + } + out.Registry = data + } + + manifest := translateEdgeletControlPlane(cp, ctrl, tOpts) + data, err := yaml.Marshal(manifest) + if err != nil { + return out, err + } + out.ControlPlane = data + return out, nil +} + +// TranslateEdgeletControlPlaneManifest returns the edgelet ControlPlane manifest struct (test helper). +// +//nolint:revive // test helper intentionally returns package-private manifest type +func TranslateEdgeletControlPlaneManifest(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, opts TranslateOptions) edgeletControlPlaneManifest { + return translateEdgeletControlPlane(cp, ctrl, opts.mergeDefaults(opts.Namespace)) +} + +// TranslateEdgeletRegistryManifest returns the edgelet Registry manifest struct (test helper). +// +//nolint:revive // test helper intentionally returns package-private manifest type +func TranslateEdgeletRegistryManifest(cp *rsc.RemoteControlPlane) (edgeletRegistryManifest, error) { + if !NeedsPrivateEdgeletRegistry(cp) { + return edgeletRegistryManifest{}, util.NewError("Remote Control Plane does not require a private edgelet registry") + } + return translateEdgeletRegistry(cp) +} + +func translateEdgeletRegistry(cp *rsc.RemoteControlPlane) (edgeletRegistryManifest, error) { + pkg := cp.Controller.Package + return edgeletRegistryManifest{ + APIVersion: edgeletAPIVersion, + Kind: edgeletRegistryKind, + Spec: edgeletRegistryManifestSpec{ + URL: pkg.Registry, + Username: pkg.Username, + Password: pkg.Password, + Email: pkg.Email, + Private: true, + }, + }, nil +} + +func translateEdgeletControlPlane(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteController, opts translateOptions) edgeletControlPlaneManifest { + sys := cp.SystemMicroservices + spec := edgeletControlPlaneManifestSpec{ + Controller: edgeletControllerSpec{ + Image: controllerImage(cp, opts.controllerImage), + Registry: opts.registryID, + PublicURL: resolvePublicURL(cp), + TrustProxy: cp.Controller.TrustProxy, + }, + Auth: authToEdgelet(cp.Auth), + LogLevel: cp.Controller.LogLevel, + } + if console := consoleToEdgelet(cp.Controller); console != nil { + spec.Console = console + } + if db := databaseToEdgelet(cp.Database); db != nil { + spec.Database = db + } + if ev := eventsToEdgelet(cp.Events); ev != nil { + spec.Events = ev + } + if sysManifest := systemMicroservicesToEdgelet(sys, opts); sysManifest != nil { + spec.SystemMicroservices = sysManifest + } + if nats := natsToEdgelet(cp.Nats); nats != nil { + spec.Nats = nats + } + if tls := tlsToEdgelet(EffectiveControllerTLS(cp, ctrl)); tls != nil { + spec.TLS = tls + } + if vault := vaultToEdgelet(cp.Vault); vault != nil { + spec.Vault = vault + } + return edgeletControlPlaneManifest{ + APIVersion: edgeletAPIVersion, + Kind: edgeletControlPlaneKind, + Metadata: edgeletControlPlaneMetadata{ + Name: opts.name, + Namespace: opts.namespace, + }, + Spec: spec, + } +} + +func controllerImage(cp *rsc.RemoteControlPlane, defaultImage string) string { + if cp.Controller.Package != nil && cp.Controller.Package.Image != "" { + return cp.Controller.Package.Image + } + return defaultImage +} + +func resolvePublicURL(cp *rsc.RemoteControlPlane) string { + if cp.Controller.PublicUrl != "" { + return cp.Controller.PublicUrl + } + return cp.Endpoint +} + +func consoleToEdgelet(ctrl rsc.LocalControllerSpec) *edgeletConsoleSpec { + if ctrl.ConsoleUrl == "" && ctrl.ConsolePort == 0 { + return nil + } + out := &edgeletConsoleSpec{URL: ctrl.ConsoleUrl} + if ctrl.ConsolePort != 0 { + out.Port = &ctrl.ConsolePort + } + return out +} + +func authToEdgelet(a rsc.Auth) edgeletAuthSpec { + out := edgeletAuthSpec{ + Mode: a.Mode, + InsecureAllowHTTP: a.InsecureAllowHttp, + InsecureAllowBootstrapLog: a.InsecureAllowBootstrapLog, + IssuerURL: a.IssuerUrl, + ConsoleClient: a.ConsoleClient, + ConsoleClientEnabled: a.ConsoleClientEnabled, + } + if a.Bootstrap != nil { + out.Bootstrap = &edgeletAuthBootstrap{ + Username: a.Bootstrap.Username, + Password: a.Bootstrap.Password, + } + } + if a.Client != nil { + out.Client = &edgeletAuthClient{ + ID: a.Client.ID, + Secret: a.Client.Secret, + } + } + if a.RateLimit != nil { + out.RateLimit = &edgeletAuthRateLimit{ + Enabled: a.RateLimit.Enabled, + MaxRequestsPerWindow: a.RateLimit.MaxRequestsPerWindow, + WindowMs: a.RateLimit.WindowMs, + } + } + if a.SessionStore != nil { + out.SessionStore = &edgeletAuthSessionStore{ + Type: a.SessionStore.Type, + TTLMs: a.SessionStore.TtlMs, + Secret: a.SessionStore.Secret, + } + } + if a.TokenTtl != nil { + out.TokenTTL = &edgeletAuthTokenTTL{ + AccessTokenTTLSeconds: a.TokenTtl.AccessTokenTtlSeconds, + RefreshTokenTTLSeconds: a.TokenTtl.RefreshTokenTtlSeconds, + } + } + if a.OidcTtl != nil { + out.OIDCTTL = &edgeletAuthOIDCTTL{ + InteractionTTLSeconds: a.OidcTtl.InteractionTtlSeconds, + GrantTTLSeconds: a.OidcTtl.GrantTtlSeconds, + SessionTTLSeconds: a.OidcTtl.SessionTtlSeconds, + IDTokenTTLSeconds: a.OidcTtl.IdTokenTtlSeconds, + } + } + return out +} + +func databaseToEdgelet(db rsc.Database) *edgeletDatabaseSpec { + if db.Provider == "" { + return nil + } + return &edgeletDatabaseSpec{ + Provider: db.Provider, + User: db.User, + Host: db.Host, + Port: db.Port, + Password: db.Password, + DatabaseName: db.DatabaseName, + SSL: db.SSL, + CA: db.CA, + } +} + +func eventsToEdgelet(ev rsc.Events) *edgeletEventsSpec { + if ev.AuditEnabled == nil && ev.RetentionDays == 0 && ev.CleanupInterval == 0 && ev.CaptureIpAddress == nil { + return nil + } + return &edgeletEventsSpec{ + AuditEnabled: ev.AuditEnabled, + RetentionDays: ev.RetentionDays, + CleanupInterval: ev.CleanupInterval, + CaptureIPAddress: ev.CaptureIpAddress, + } +} + +func systemMicroservicesToEdgelet(sys install.RemoteSystemMicroservices, opts translateOptions) *edgeletSystemMicroservices { + router := remoteImagesToArchMap(sys.Router, opts.routerImage) + nats := remoteImagesToArchMap(sys.Nats, opts.natsImage) + if len(router) == 0 && len(nats) == 0 { + return nil + } + return &edgeletSystemMicroservices{ + Router: router, + Nats: nats, + } +} + +func remoteImagesToArchMap(images install.RemoteSystemImages, defaultImage string) map[string]string { + out := map[string]string{} + if images.AMD64 != "" { + out["amd64"] = images.AMD64 + } + if images.ARM64 != "" { + out["arm64"] = images.ARM64 + } + if images.RISCV64 != "" { + out["riscv64"] = images.RISCV64 + } + if images.ARM != "" { + out["arm"] = images.ARM + } + if len(out) == 0 && defaultImage != "" { + out = defaultArchImages(defaultImage) + } + return out +} + +func defaultArchImages(image string) map[string]string { + return map[string]string{ + "amd64": image, + "arm64": image, + "riscv64": image, + "arm": image, + } +} + +func natsToEdgelet(n *rsc.NatsEnabledConfig) *edgeletNatsSpec { + if n == nil { + return nil + } + return &edgeletNatsSpec{Enabled: n.Enabled} +} + +func tlsToEdgelet(t *rsc.ControlPlaneTLS) *edgeletTLSSpec { + if t == nil || (t.CA == "" && t.Cert == "" && t.Key == "") { + return nil + } + return &edgeletTLSSpec{ + Base64: &edgeletTLSBase64{ + CA: t.CA, + Cert: t.Cert, + Key: t.Key, + }, + } +} + +func vaultToEdgelet(v *rsc.VaultSpec) *edgeletVaultSpec { + if v == nil { + return nil + } + out := &edgeletVaultSpec{ + Enabled: v.Enabled, + Provider: v.Provider, + BasePath: v.BasePath, + } + if v.Hashicorp != nil { + out.Hashicorp = &edgeletVaultHashicorp{ + Address: v.Hashicorp.Address, + Token: v.Hashicorp.Token, + Mount: v.Hashicorp.Mount, + } + } + if v.Aws != nil { + out.Aws = &edgeletVaultAws{ + Region: v.Aws.Region, + AccessKeyID: v.Aws.AccessKeyId, + AccessKey: v.Aws.AccessKey, + } + } + if v.Azure != nil { + out.Azure = &edgeletVaultAzure{ + URL: v.Azure.URL, + TenantID: v.Azure.TenantId, + ClientID: v.Azure.ClientId, + ClientSecret: v.Azure.ClientSecret, + } + } + if v.Google != nil { + out.Google = &edgeletVaultGoogle{ + ProjectID: v.Google.ProjectId, + Credentials: v.Google.Credentials, + } + } + return out +} diff --git a/internal/deploy/controlplane/remote/translate_test.go b/internal/deploy/controlplane/remote/translate_test.go new file mode 100644 index 000000000..a8312ca04 --- /dev/null +++ b/internal/deploy/controlplane/remote/translate_test.go @@ -0,0 +1,143 @@ +package deployremotecontrolplane + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" +) + +const testNamespace = "test-ns" + +const ( + testControllerImage = "ghcr.io/datasance/controller:3.8.0-rc.1" + testRouterImage = "ghcr.io/datasance/router:3.8.0-rc.1" + testNatsImage = "ghcr.io/datasance/nats:2.14.2-rc.2" + + testIofogControllerImage = "ghcr.io/eclipse-iofog/controller:3.8.0-rc.1" + testIofogRouterImage = "ghcr.io/eclipse-iofog/router:3.8.0-rc.1" + testIofogNatsImage = "ghcr.io/eclipse-iofog/nats:2.14.2-rc.2" +) + +func resourceFixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "..", "..", "..", "resource", "testdata", "remote", name) +} + +func remoteFixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "testdata", name) +} + +func loadRemoteControlPlaneFixture(t *testing.T, name string) rsc.RemoteControlPlane { + t.Helper() + raw, err := os.ReadFile(resourceFixturePath(name)) + require.NoError(t, err) + cp, err := rsc.UnmarshallRemoteControlPlane(raw) + require.NoError(t, err) + return cp +} + +func loadExpectedControlPlane(t *testing.T, name string) edgeletControlPlaneManifest { + t.Helper() + raw, err := os.ReadFile(remoteFixturePath(name)) + require.NoError(t, err) + var manifest edgeletControlPlaneManifest + require.NoError(t, yaml.UnmarshalStrict(raw, &manifest)) + return manifest +} + +func stripManifestSecrets(m *edgeletControlPlaneManifest) { + if m.Spec.Auth.Bootstrap != nil { + m.Spec.Auth.Bootstrap.Password = "" + } +} + +func assertTranslatedControlPlane(t *testing.T, got, want edgeletControlPlaneManifest) { + t.Helper() + stripManifestSecrets(&got) + stripManifestSecrets(&want) + require.Equal(t, want, got) +} + +func datasanceTranslateOptions() TranslateOptions { + return TranslateOptions{ + Name: "iofog", + Namespace: testNamespace, + ControllerImage: testControllerImage, + RouterImage: testRouterImage, + NatsImage: testNatsImage, + } +} + +func iofogTranslateOptions() TranslateOptions { + return TranslateOptions{ + Name: "iofog", + Namespace: testNamespace, + ControllerImage: testIofogControllerImage, + RouterImage: testIofogRouterImage, + NatsImage: testIofogNatsImage, + } +} + +func TestTranslateEdgeletControlPlane_DatasanceGolden(t *testing.T) { + cp := loadRemoteControlPlaneFixture(t, "controlplane-datasance.yaml") + ctrl := cp.Controllers[0] + got := TranslateEdgeletControlPlaneManifest(&cp, &ctrl, datasanceTranslateOptions()) + want := loadExpectedControlPlane(t, "edgelet-cp-datasance.yaml") + assertTranslatedControlPlane(t, got, want) +} + +func TestTranslateEdgeletControlPlane_IofogGolden(t *testing.T) { + cp := loadRemoteControlPlaneFixture(t, "controlplane-iofog.yaml") + ctrl := cp.Controllers[0] + got := TranslateEdgeletControlPlaneManifest(&cp, &ctrl, iofogTranslateOptions()) + want := loadExpectedControlPlane(t, "edgelet-cp-iofog.yaml") + assertTranslatedControlPlane(t, got, want) +} + +func TestTranslateEdgeletControlPlane_PerControllerTLSOverride(t *testing.T) { + cp := loadRemoteControlPlaneFixture(t, "controlplane-datasance.yaml") + cp.TLS = &rsc.ControlPlaneTLS{ + CA: "global-ca", + Cert: "global-cert", + Key: "global-key", + } + ctrl := cp.Controllers[0] + ctrl.TLS = &rsc.ControlPlaneTLS{ + CA: "host-ca", + Cert: "host-cert", + Key: "host-key", + } + + got := TranslateEdgeletControlPlaneManifest(&cp, &ctrl, datasanceTranslateOptions()) + require.NotNil(t, got.Spec.TLS) + require.NotNil(t, got.Spec.TLS.Base64) + require.Equal(t, "host-ca", got.Spec.TLS.Base64.CA) + require.Equal(t, "host-cert", got.Spec.TLS.Base64.Cert) + require.Equal(t, "host-key", got.Spec.TLS.Base64.Key) +} + +func TestTranslateEdgeletControlPlane_StripsCLIOnlyFields(t *testing.T) { + cp := loadRemoteControlPlaneFixture(t, "controlplane-datasance.yaml") + ctrl := cp.Controllers[0] + result, err := TranslateRemoteControlPlane(&cp, &ctrl, datasanceTranslateOptions()) + require.NoError(t, err) + require.Nil(t, result.Registry) + body := string(result.ControlPlane) + require.NotContains(t, body, "iofogUser") + require.NotContains(t, body, "systemAgent") + require.NotContains(t, body, "controllers:") +} + +func TestResolveEdgeletRegistryID_AirgapDefault(t *testing.T) { + cp := loadRemoteControlPlaneFixture(t, "controlplane-datasance.yaml") + cp.Airgap = true + got := ResolveEdgeletRegistryID(&cp, nil) + require.NotNil(t, got) + require.Equal(t, EdgeletRegistryAirgap, *got) +} diff --git a/internal/deploy/edgeresource/factory.go b/internal/deploy/edgeresource/factory.go deleted file mode 100644 index de5c91b31..000000000 --- a/internal/deploy/edgeresource/factory.go +++ /dev/null @@ -1,94 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package deployedgeresource - -import ( - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/internal/config" - "github.com/eclipse-iofog/iofogctl/internal/execute" - rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" - yaml "gopkg.in/yaml.v2" -) - -type Options struct { - Namespace string - Name string - Yaml []byte -} - -type executor struct { - namespace string - name string - edge rsc.EdgeResource -} - -func (exe *executor) GetName() string { - return "deploying Edge Resource " + exe.name -} - -func (exe *executor) Execute() (err error) { - if _, err = config.GetNamespace(exe.namespace); err != nil { - return - } - - // Translate edge resource to client type - edge := &client.EdgeResourceMetadata{ - Name: exe.name, - Description: exe.edge.Description, - Version: exe.edge.Version, - InterfaceProtocol: exe.edge.InterfaceProtocol, - Display: exe.edge.Display, - OrchestrationTags: exe.edge.OrchestrationTags, - Interface: client.HTTPEdgeResource{ - Endpoints: exe.edge.Interface.Endpoints, - }, - Custom: exe.edge.Custom, - } - // Connect to Controller - clt, err := clientutil.NewControllerClient(exe.namespace) - if err != nil { - return - } - - // Create the resource - if err = clt.UpdateHTTPEdgeResource(edge.Name, edge); err != nil { - return - } - - return -} - -func NewExecutor(opt Options) (execute.Executor, error) { - // Unmarshal file - var edge rsc.EdgeResource - if err := yaml.UnmarshalStrict(opt.Yaml, &edge); err != nil { - err = util.NewUnmarshalError(err.Error()) - return nil, err - } - // Validate input - if opt.Name == "" { - return nil, util.NewInputError("Did not specify metadata.name") - } - if err := util.IsLowerAlphanumeric("Edge Resource", opt.Name); err != nil { - return nil, err - } - // Return executor - return &executor{ - namespace: opt.Namespace, - name: opt.Name, - edge: edge, - }, nil -} diff --git a/internal/deploy/execute.go b/internal/deploy/execute.go index 42b57bda2..cb6b1cfcc 100644 --- a/internal/deploy/execute.go +++ b/internal/deploy/execute.go @@ -1,20 +1,8 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deploy import ( "fmt" + "strings" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/config" @@ -30,7 +18,6 @@ import ( deployk8scontrolplane "github.com/eclipse-iofog/iofogctl/internal/deploy/controlplane/k8s" deploylocalcontrolplane "github.com/eclipse-iofog/iofogctl/internal/deploy/controlplane/local" deployremotecontrolplane "github.com/eclipse-iofog/iofogctl/internal/deploy/controlplane/remote" - deployedgeresource "github.com/eclipse-iofog/iofogctl/internal/deploy/edgeresource" deploymicroservice "github.com/eclipse-iofog/iofogctl/internal/deploy/microservice" deploynatsaccountrule "github.com/eclipse-iofog/iofogctl/internal/deploy/natsaccountrule" deploynatsuserrule "github.com/eclipse-iofog/iofogctl/internal/deploy/natsuserrule" @@ -41,6 +28,7 @@ import ( deploysecret "github.com/eclipse-iofog/iofogctl/internal/deploy/secret" deployservice "github.com/eclipse-iofog/iofogctl/internal/deploy/service" deployserviceaccount "github.com/eclipse-iofog/iofogctl/internal/deploy/serviceaccount" + deployvalidate "github.com/eclipse-iofog/iofogctl/internal/deploy/validate" deployvolume "github.com/eclipse-iofog/iofogctl/internal/deploy/volume" deployvolumemount "github.com/eclipse-iofog/iofogctl/internal/deploy/volumeMount" "github.com/eclipse-iofog/iofogctl/internal/execute" @@ -63,7 +51,6 @@ var kindOrder = []config.Kind{ config.NatsUserRuleKind, config.RemoteAgentKind, config.LocalAgentKind, - config.EdgeResourceKind, config.ApplicationTemplateKind, config.VolumeKind, config.OfflineImageKind, @@ -82,10 +69,6 @@ type Options struct { TransferPool int } -func deployEdgeResource(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { - return deployedgeresource.NewExecutor(deployedgeresource.Options{Namespace: opt.Namespace, Yaml: opt.YAML, Name: opt.Name}) -} - func deployCatalogItem(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { return deploycatalogitem.NewExecutor(deploycatalogitem.Options{Namespace: opt.Namespace, Yaml: opt.YAML, Name: opt.Name}) } @@ -111,7 +94,7 @@ func deployRemoteControlPlane(opt *execute.KindHandlerOpt) (exe execute.Executor } func deployLocalControlPlane(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { - return deploylocalcontrolplane.NewExecutor(deploylocalcontrolplane.Options{Namespace: opt.Namespace, Yaml: opt.YAML, Name: opt.Name}) + return deploylocalcontrolplane.NewExecutor(deploylocalcontrolplane.Options{Namespace: opt.Namespace, Yaml: opt.YAML, FullYAML: opt.FullYAML, Name: opt.Name}) } func deployRemoteController(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { @@ -195,46 +178,31 @@ func deployNatsUserRule(opt *execute.KindHandlerOpt) (exe execute.Executor, err // Execute deploy from yaml file func Execute(opt *Options) (err error) { kindHandlers := buildKindHandlers(opt.NoCache, opt.TransferPool) - executorsMap, err := execute.GetExecutorsFromYAML(opt.InputFile, opt.Namespace, kindHandlers) + executorsMap, err := execute.GetExecutorsFromYAML(opt.InputFile, opt.Namespace, kindHandlers, false) if err != nil { return err } - // Create any AgentConfig executor missing - // Each Agent requires a corresponding Agent Config to be created with Controller + // Create any AgentConfig executor missing. + // Each Agent requires a corresponding Agent Config to be created with Controller. + // CP system agents are registered by LocalControlPlane deploy (systemAgent path), not here. appendedAgentExecs := append(executorsMap[config.LocalAgentKind], executorsMap[config.RemoteAgentKind]...) - // Check if control plane is LocalControlPlane (either already in namespace or being deployed) - var isLocalControlPlane bool - if len(executorsMap[config.LocalControlPlaneKind]) > 0 { - // LocalControlPlane is being deployed in this execution - isLocalControlPlane = true - } else { - // Check if LocalControlPlane already exists in namespace - ns, err := config.GetNamespace(opt.Namespace) - if err == nil { - controlPlane, err := ns.GetControlPlane() - if err == nil { - _, isLocalControlPlane = controlPlane.(*rsc.LocalControlPlane) - } - } - } for _, agentGenericExecutor := range appendedAgentExecs { agentExecutor, ok := agentGenericExecutor.(deployagent.AgentDeployExecutor) if !ok { return util.NewInternalError("Could not convert agent deploy executor\n") } - found := false - host := agentExecutor.GetHost() + + specHost := agentExecutor.GetHost() tags := agentExecutor.GetTags() deployConfig := agentExecutor.GetConfig() - // Determine the host value to send to the Controller (AgentConfiguration.Host). - // Prefer the host explicitly set in the agent configuration; otherwise, fall back to the spec host. - apiHost := host - if deployConfig != nil && deployConfig.AgentConfiguration.Host != nil && *deployConfig.AgentConfiguration.Host != "" { - apiHost = *deployConfig.AgentConfiguration.Host + apiHost := specHost + if deployConfig != nil && deployConfig.Host != nil && strings.TrimSpace(*deployConfig.Host) != "" { + apiHost = strings.TrimSpace(*deployConfig.Host) } + found := false for _, configGenericExecutor := range executorsMap[config.AgentConfigKind] { configExecutor, ok := configGenericExecutor.(deployagentconfig.AgentConfigExecutor) if !ok { @@ -247,66 +215,39 @@ func Execute(opt *Options) (err error) { break } } - if !found { - agentConfig := client.AgentConfiguration{ - Host: &apiHost, - } - if util.IsLocalHost(host) && isLocalControlPlane { // Set de default local config to interior standalone for LocalControlPlane - isSystem := true - deploymentType := "container" - upstreamRouters := []string{} - routerMode := iofog.RouterModeInterior - edgeRouterPort := 45671 - interRouterPort := 55671 - upstreamNatsServers := []string{} - natsMode := iofog.NatsModeServer - natsServerPort := 4222 - natsLeafPort := 7422 - natsClusterPort := 6222 - natsMqttPort := 8883 - natsHttpPort := 8222 - jsStorageSize := "10G" - jsMemoryStoreSize := "1G" - agentConfig.IsSystem = &isSystem - agentConfig.DeploymentType = &deploymentType - agentConfig.UpstreamRouters = &upstreamRouters - agentConfig.RouterConfig = client.RouterConfig{ - RouterMode: &routerMode, - EdgeRouterPort: &edgeRouterPort, - InterRouterPort: &interRouterPort, - } - agentConfig.UpstreamNatsServers = &upstreamNatsServers - agentConfig.NatsConfig = client.NatsConfig{ - NatsMode: &natsMode, - NatsServerPort: &natsServerPort, - NatsLeafPort: &natsLeafPort, - NatsClusterPort: &natsClusterPort, - NatsMqttPort: &natsMqttPort, - NatsHttpPort: &natsHttpPort, - JsStorageSize: &jsStorageSize, - JsMemoryStoreSize: &jsMemoryStoreSize, - } - } else { - // For remote agents, use the configuration from the agent executor - if deployConfig == nil { - // Initialize default remote agent configuration - agentConfig = client.AgentConfiguration{ - Host: &apiHost, - } - } else { - agentConfig = deployConfig.AgentConfiguration - agentConfig.Host = &apiHost - } - } - executorsMap[config.AgentConfigKind] = append(executorsMap[config.AgentConfigKind], deployagentconfig.NewRemoteExecutor( + if found { + continue + } + + agentConfig := buildSyntheticAgentConfiguration(deployConfig, apiHost) + syntheticConfig := &rsc.AgentConfiguration{ + Name: agentExecutor.GetName(), + AgentConfiguration: agentConfig, + } + if err := deployagentconfig.Validate(syntheticConfig); err != nil { + return err + } + + executorsMap[config.AgentConfigKind] = append(executorsMap[config.AgentConfigKind], + deployagentconfig.NewRemoteExecutor( agentExecutor.GetName(), - &rsc.AgentConfiguration{ - Name: agentExecutor.GetName(), - AgentConfiguration: agentConfig, - }, + syntheticConfig, opt.Namespace, tags, )) + } + + if localCPExes, exists := executorsMap[config.LocalControlPlaneKind]; exists { + namespaceHasOtherCP := false + if ns, nsErr := config.GetNamespace(opt.Namespace); nsErr == nil { + if existingCP, cpErr := ns.GetControlPlane(); cpErr == nil { + if _, ok := existingCP.(*rsc.LocalControlPlane); !ok { + namespaceHasOtherCP = true + } + } + } + if err := deployvalidate.LocalControlPlaneDeploy(len(localCPExes), namespaceHasOtherCP); err != nil { + return err } } @@ -338,6 +279,12 @@ func Execute(opt *Options) (err error) { } // Controllers + if err := deployvalidate.RemoteControllerDeploy(opt.InputFile); err != nil { + return err + } + if errs := execute.RunExecutors(executorsMap[config.RemoteControllerKind], "deploy remote controller"); len(errs) > 0 { + return execute.CoalesceErrors(errs) + } if errs := execute.RunExecutors(executorsMap[config.LocalControllerKind], "deploy local controller"); len(errs) > 0 { return execute.CoalesceErrors(errs) } @@ -348,7 +295,6 @@ func Execute(opt *Options) (err error) { } // Execute in parallel by priority order - // Edge Resources, Agents, Volumes, CatalogItem, Application, Microservice, Route for idx := range kindOrder { if errs := execute.RunExecutors(executorsMap[kindOrder[idx]], fmt.Sprintf("deploy %s", kindOrder[idx])); len(errs) > 0 { return execute.CoalesceErrors(errs) @@ -364,7 +310,6 @@ func buildKindHandlers(noCache bool, transferPool int) map[config.Kind]func(*exe config.ApplicationTemplateKind: deployApplicationTemplate, config.MicroserviceKind: deployMicroservice, config.CatalogItemKind: deployCatalogItem, - config.EdgeResourceKind: deployEdgeResource, config.KubernetesControlPlaneKind: deployKubernetesControlPlane, config.RemoteControlPlaneKind: deployRemoteControlPlane, config.LocalControlPlaneKind: deployLocalControlPlane, @@ -568,3 +513,20 @@ func mapUUIDsToNames(uuids []string, agentByUUID map[string]*client.AgentInfo) ( } return } + +func buildSyntheticAgentConfiguration(deployConfig *rsc.AgentConfiguration, apiHost string) client.AgentConfiguration { + host := apiHost + isSystem := false + + if deployConfig == nil { + return client.AgentConfiguration{ + Host: &host, + IsSystem: &isSystem, + } + } + + cfg := deployConfig.AgentConfiguration + cfg.Host = &host + cfg.IsSystem = &isSystem + return cfg +} diff --git a/internal/deploy/execute_agent_config_test.go b/internal/deploy/execute_agent_config_test.go new file mode 100644 index 000000000..00cfeded9 --- /dev/null +++ b/internal/deploy/execute_agent_config_test.go @@ -0,0 +1,35 @@ +package deploy + +import ( + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + iutil "github.com/eclipse-iofog/iofogctl/internal/util" + "github.com/stretchr/testify/require" +) + +func TestBuildSyntheticAgentConfiguration_ForcesNonSystemAgent(t *testing.T) { + cfg := buildSyntheticAgentConfiguration(nil, "192.168.139.27") + require.NotNil(t, cfg.Host) + require.Equal(t, "192.168.139.27", *cfg.Host) + require.NotNil(t, cfg.IsSystem) + require.False(t, *cfg.IsSystem) +} + +func TestBuildSyntheticAgentConfiguration_PreservesInlineConfig(t *testing.T) { + deployConfig := &rsc.AgentConfiguration{ + AgentConfiguration: client.AgentConfiguration{ + DeploymentType: iutil.MakeStrPtr("native"), + ContainerEngine: iutil.MakeStrPtr("edgelet"), + }, + } + + cfg := buildSyntheticAgentConfiguration(deployConfig, "192.168.139.27") + require.NotNil(t, cfg.IsSystem) + require.False(t, *cfg.IsSystem) + require.NotNil(t, cfg.DeploymentType) + require.Equal(t, "native", *cfg.DeploymentType) + require.NotNil(t, cfg.ContainerEngine) + require.Equal(t, "edgelet", *cfg.ContainerEngine) +} diff --git a/internal/deploy/microservice/factory.go b/internal/deploy/microservice/factory.go index b406184e4..1dd94fb41 100644 --- a/internal/deploy/microservice/factory.go +++ b/internal/deploy/microservice/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deploymicroservice import ( diff --git a/internal/deploy/natsaccountrule/factory.go b/internal/deploy/natsaccountrule/factory.go index 6d09235ff..1945db44f 100644 --- a/internal/deploy/natsaccountrule/factory.go +++ b/internal/deploy/natsaccountrule/factory.go @@ -2,6 +2,7 @@ package deploynatsaccountrule import ( "bytes" + "errors" "fmt" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" @@ -42,7 +43,8 @@ func (exe *executor) Execute() error { if err == nil { return nil } - if _, ok := err.(*client.NotFoundError); !ok { + notFoundError := &client.NotFoundError{} + if errors.As(err, ¬FoundError) { return err } @@ -80,7 +82,7 @@ func NewExecutor(opt Options) (execute.Executor, error) { var specMap map[interface{}]interface{} if err = yaml.Unmarshal(opt.Yaml, &specMap); err == nil { doc := map[interface{}]interface{}{ - "apiVersion": "iofog.org/v3", + "apiVersion": util.GetCliApiVersion(), "kind": "NatsAccountRule", "metadata": map[interface{}]interface{}{"name": opt.Name}, "spec": specMap, diff --git a/internal/deploy/natsuserrule/factory.go b/internal/deploy/natsuserrule/factory.go index bd01d119c..dd47d769a 100644 --- a/internal/deploy/natsuserrule/factory.go +++ b/internal/deploy/natsuserrule/factory.go @@ -2,6 +2,7 @@ package deploynatsuserrule import ( "bytes" + "errors" "fmt" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" @@ -41,7 +42,8 @@ func (exe *executor) Execute() error { if err == nil { return nil } - if _, ok := err.(*client.NotFoundError); !ok { + notFoundError := &client.NotFoundError{} + if errors.As(err, ¬FoundError) { return err } @@ -79,7 +81,7 @@ func NewExecutor(opt Options) (execute.Executor, error) { var specMap map[interface{}]interface{} if err = yaml.Unmarshal(opt.Yaml, &specMap); err == nil { doc := map[interface{}]interface{}{ - "apiVersion": "iofog.org/v3", + "apiVersion": util.GetCliApiVersion(), "kind": "NatsUserRule", "metadata": map[interface{}]interface{}{"name": opt.Name}, "spec": specMap, diff --git a/internal/deploy/offlineimage/executor.go b/internal/deploy/offlineimage/executor.go index 634ad53f9..7b9cb1cf5 100644 --- a/internal/deploy/offlineimage/executor.go +++ b/internal/deploy/offlineimage/executor.go @@ -118,7 +118,7 @@ func validateDefinition(def *rsc.OfflineImage) error { if len(def.Agents) == 0 { return util.NewInputError("OfflineImage spec must include at least one agent entry") } - if def.X86Image == "" && def.ArmImage == "" { + if def.AMD64Image == "" && def.ARM64Image == "" && def.RISCV64Image == "" && def.ArmImage == "" { return util.NewInputError("OfflineImage spec must include at least one architecture image (x86 or arm)") } if def.Auth != nil { @@ -147,7 +147,7 @@ func (exe *executor) buildAgentPlans(ns *rsc.Namespace) ([]agentPlan, error) { if err != nil { return nil, err } - platform, err := resolvePlatform(cfg.FogType) + platform, err := resolvePlatform(cfg.Arch) if err != nil { return nil, fmt.Errorf("agent %s: %w", agentName, err) } @@ -155,7 +155,7 @@ func (exe *executor) buildAgentPlans(ns *rsc.Namespace) ([]agentPlan, error) { if err != nil { return nil, fmt.Errorf("agent %s: %w", agentName, err) } - engine, err := resolveContainerEngine(cfg.AgentConfiguration.ContainerEngine) + engine, err := resolveContainerEngine(cfg.ContainerEngine) if err != nil { return nil, fmt.Errorf("agent %s: %w", agentName, err) } @@ -172,13 +172,23 @@ func (exe *executor) buildAgentPlans(ns *rsc.Namespace) ([]agentPlan, error) { func (exe *executor) imageForPlatform(platform string) (string, error) { switch platform { case platformAMD64: - if exe.spec.X86Image == "" { + if exe.spec.AMD64Image == "" { return "", util.NewInputError("x86 image is required for agents with linux/amd64 fog type") } - return exe.spec.X86Image, nil + return exe.spec.AMD64Image, nil case platformARM64: + if exe.spec.ARM64Image == "" { + return "", util.NewInputError("arm64 image is required for agents with linux/arm64 fog type") + } + return exe.spec.ARM64Image, nil + case platformRISCV64: + if exe.spec.RISCV64Image == "" { + return "", util.NewInputError("riscv64 image is required for agents with linux/riscv64 fog type") + } + return exe.spec.RISCV64Image, nil + case platformARM: if exe.spec.ArmImage == "" { - return "", util.NewInputError("arm image is required for agents with linux/arm64 fog type") + return "", util.NewInputError("arm image is required for agents with linux/arm fog type") } return exe.spec.ArmImage, nil default: @@ -217,16 +227,28 @@ func (exe *executor) registerCatalogItem() error { } images := []client.CatalogImage{} - if exe.spec.X86Image != "" { + if exe.spec.AMD64Image != "" { + images = append(images, client.CatalogImage{ + ContainerImage: exe.spec.AMD64Image, + ArchID: client.ArchNameToID["amd64"], + }) + } + if exe.spec.ARM64Image != "" { + images = append(images, client.CatalogImage{ + ContainerImage: exe.spec.ARM64Image, + ArchID: client.ArchNameToID["arm64"], + }) + } + if exe.spec.RISCV64Image != "" { images = append(images, client.CatalogImage{ - ContainerImage: exe.spec.X86Image, - AgentTypeID: client.AgentTypeAgentTypeIDDict["x86"], + ContainerImage: exe.spec.RISCV64Image, + ArchID: client.ArchNameToID["riscv64"], }) } if exe.spec.ArmImage != "" { images = append(images, client.CatalogImage{ ContainerImage: exe.spec.ArmImage, - AgentTypeID: client.AgentTypeAgentTypeIDDict["arm"], + ArchID: client.ArchNameToID["arm"], }) } item, err := clt.GetCatalogItemByName(exe.spec.Name) @@ -270,7 +292,7 @@ func (exe *executor) confirmCatalogUpdate(item *client.CatalogItemInfo) (bool, e if len(item.Images) > 0 { segments := make([]string, 0, len(item.Images)) for _, img := range item.Images { - segments = append(segments, fmt.Sprintf("%s (AgentTypeID: %d)", img.ContainerImage, img.AgentTypeID)) + segments = append(segments, fmt.Sprintf("%s (ArchID: %d)", img.ContainerImage, img.ArchID)) } imageDetails = strings.Join(segments, " | ") } diff --git a/internal/deploy/offlineimage/helpers.go b/internal/deploy/offlineimage/helpers.go index 6613f5852..f85310ff2 100644 --- a/internal/deploy/offlineimage/helpers.go +++ b/internal/deploy/offlineimage/helpers.go @@ -8,8 +8,10 @@ import ( ) const ( - platformAMD64 = "linux/amd64" - platformARM64 = "linux/arm64" + platformAMD64 = "linux/amd64" + platformARM64 = "linux/arm64" + platformRISCV64 = "linux/riscv64" + platformARM = "linux/arm" ) type agentPlan struct { @@ -30,18 +32,18 @@ func (e containerEngine) command() string { return string(e) } -func resolvePlatform(fogType *string) (string, error) { - if fogType == nil { +func resolvePlatform(arch *string) (string, error) { + if arch == nil { return "", util.NewInputError("Agent fog type is not configured in Controller") } - value := strings.ToLower(strings.TrimSpace(*fogType)) + value := strings.ToLower(strings.TrimSpace(*arch)) switch value { case "1", "x86", "amd64", platformAMD64: return platformAMD64, nil case "2", "arm", "arm64", platformARM64: return platformARM64, nil default: - return "", util.NewInputError("Unsupported fog type " + *fogType) + return "", util.NewInputError("Unsupported fog type " + *arch) } } diff --git a/internal/deploy/offlineimage/images.go b/internal/deploy/offlineimage/images.go index 48a11e82a..1732b9137 100644 --- a/internal/deploy/offlineimage/images.go +++ b/internal/deploy/offlineimage/images.go @@ -13,15 +13,15 @@ import ( "strings" "time" - "github.com/containers/image/v5/copy" - "github.com/containers/image/v5/manifest" - "github.com/containers/image/v5/signature" - "github.com/containers/image/v5/transports/alltransports" - "github.com/containers/image/v5/types" "github.com/eclipse-iofog/iofogctl/internal/config" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" "github.com/eclipse-iofog/iofogctl/pkg/util" "github.com/opencontainers/go-digest" + "go.podman.io/image/v5/copy" + "go.podman.io/image/v5/manifest" + "go.podman.io/image/v5/signature" + "go.podman.io/image/v5/transports/alltransports" + "go.podman.io/image/v5/types" ) type imageArtifact struct { @@ -54,7 +54,7 @@ func (exe *executor) ensureArtifact(ctx context.Context, platform, imageRef stri } cacheDir := config.GetOfflineImageCacheDir(exe.namespace, exe.spec.Name, sanitizeSegment(platform)) - if err := os.MkdirAll(cacheDir, 0o755); err != nil { + if err := os.MkdirAll(cacheDir, util.DirPerm); err != nil { return nil, err } archivePath := filepath.Join(cacheDir, archiveFilename) @@ -152,7 +152,7 @@ func buildSystemContext(platform string, auth *rsc.OfflineImageAuth) (*types.Sys func pullCompressedImage(ctx context.Context, imageRef, archivePath string, sysCtx *types.SystemContext, label string) (digestValue string, checksum string, size int64, err error) { destDir := filepath.Dir(archivePath) - if err := os.MkdirAll(destDir, 0o755); err != nil { + if err := os.MkdirAll(destDir, util.DirPerm); err != nil { return "", "", 0, err } rawPath := archivePath + ".raw" @@ -181,7 +181,7 @@ func pullCompressedImage(ctx context.Context, imageRef, archivePath string, sysC if err != nil { return "", "", 0, err } - defer policyCtx.Destroy() + defer func() { _ = policyCtx.Destroy() }() progressCh := make(chan types.ProgressProperties, 1) progressDone := startProgressTracker(label, progressCh) @@ -295,21 +295,21 @@ func insecurePolicyContext() (*signature.PolicyContext, error) { } func compressToGzip(src, dst string) error { - source, err := os.Open(src) + source, err := util.OpenValidatedFile(src) if err != nil { return err } - defer source.Close() + defer util.IgnoreClose(source) if err := os.RemoveAll(dst); err != nil && !os.IsNotExist(err) { return err } - destFile, err := os.Create(dst) + destFile, err := util.CreateUserFile(dst, util.FilePerm) if err != nil { return err } - defer destFile.Close() + defer util.IgnoreClose(destFile) gzipWriter := gzip.NewWriter(destFile) defer gzipWriter.Close() @@ -322,11 +322,11 @@ func compressToGzip(src, dst string) error { } func calculateFileChecksum(path string) (string, int64, error) { - file, err := os.Open(path) + file, err := util.OpenValidatedFile(path) if err != nil { return "", 0, err } - defer file.Close() + defer util.IgnoreClose(file) hasher := sha256.New() size, err := io.Copy(hasher, file) @@ -337,7 +337,7 @@ func calculateFileChecksum(path string) (string, int64, error) { } func loadCacheMetadata(path string) (*cacheMetadata, error) { - data, err := os.ReadFile(path) + data, err := util.ReadValidatedFile(path) if err != nil { return nil, err } @@ -353,5 +353,5 @@ func saveCacheMetadata(path string, meta cacheMetadata) error { if err != nil { return err } - return os.WriteFile(path, data, 0o644) + return util.WriteValidatedFile(path, data, util.FilePerm) } diff --git a/internal/deploy/offlineimage/progress.go b/internal/deploy/offlineimage/progress.go index 4841a6a3c..6d7555cfc 100644 --- a/internal/deploy/offlineimage/progress.go +++ b/internal/deploy/offlineimage/progress.go @@ -4,8 +4,8 @@ import ( "io" "sync/atomic" - "github.com/containers/image/v5/types" "github.com/eclipse-iofog/iofogctl/pkg/util" + "go.podman.io/image/v5/types" ) type progressPrinter struct { diff --git a/internal/deploy/offlineimage/transfer.go b/internal/deploy/offlineimage/transfer.go index 7bd94a9c9..ed9c85556 100644 --- a/internal/deploy/offlineimage/transfer.go +++ b/internal/deploy/offlineimage/transfer.go @@ -2,7 +2,6 @@ package deployofflineimage import ( "fmt" - "os" "strings" "github.com/eclipse-iofog/iofogctl/pkg/util" @@ -26,11 +25,11 @@ func transferArtifact(plan agentPlan, artifact *imageArtifact) error { return err } - file, err := os.Open(artifact.path) + file, err := util.OpenValidatedFile(artifact.path) if err != nil { return err } - defer file.Close() + defer util.IgnoreClose(file) info, err := file.Stat() if err != nil { return err diff --git a/internal/deploy/registry/factory.go b/internal/deploy/registry/factory.go index c325f38a9..462c0098c 100644 --- a/internal/deploy/registry/factory.go +++ b/internal/deploy/registry/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployregistry import ( diff --git a/internal/deploy/role/factory.go b/internal/deploy/role/factory.go index a5ef25f96..6064b5866 100644 --- a/internal/deploy/role/factory.go +++ b/internal/deploy/role/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployrole import ( diff --git a/internal/deploy/rolebinding/factory.go b/internal/deploy/rolebinding/factory.go index 3bc553951..85220fc6c 100644 --- a/internal/deploy/rolebinding/factory.go +++ b/internal/deploy/rolebinding/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployrolebinding import ( diff --git a/internal/deploy/secret/factory.go b/internal/deploy/secret/factory.go index 316969724..6de3d29fb 100644 --- a/internal/deploy/secret/factory.go +++ b/internal/deploy/secret/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deploysecret import ( diff --git a/internal/deploy/service/factory.go b/internal/deploy/service/factory.go index 3cd55d757..a1224d281 100644 --- a/internal/deploy/service/factory.go +++ b/internal/deploy/service/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployservice import ( @@ -25,6 +12,8 @@ import ( "gopkg.in/yaml.v2" ) +const serviceHubReadyMessage = "Service hub provisioned; edge bridge listeners on tagged fogs may still be converging." + type Options struct { Namespace string Yaml []byte @@ -64,7 +53,7 @@ func (exe *executor) updateService(clt *client.Client) (err error) { return err } - return nil + return clientutil.WaitForServiceProvisioningReadyWithRetry(exe.namespace, exe.name) } func (exe *executor) createService(clt *client.Client) (err error) { @@ -82,7 +71,7 @@ func (exe *executor) createService(clt *client.Client) (err error) { if err = clt.CreateService(&request); err != nil { return err } - return nil + return clientutil.WaitForServiceProvisioningReadyWithRetry(exe.namespace, exe.name) } func (exe *executor) Execute() error { @@ -93,9 +82,17 @@ func (exe *executor) Execute() error { return err } if _, err = clt.GetService(exe.name); err != nil { - return exe.createService(clt) + if err = exe.createService(clt); err != nil { + return err + } + util.PrintInfo(serviceHubReadyMessage) + return nil } - return exe.updateService(clt) + if err = exe.updateService(clt); err != nil { + return err + } + util.PrintInfo(serviceHubReadyMessage) + return nil } func NewExecutor(opt Options) (exe execute.Executor, err error) { diff --git a/internal/deploy/serviceaccount/factory.go b/internal/deploy/serviceaccount/factory.go index b80698400..e12c69715 100644 --- a/internal/deploy/serviceaccount/factory.go +++ b/internal/deploy/serviceaccount/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployserviceaccount import ( diff --git a/internal/deploy/validate/local_agent.go b/internal/deploy/validate/local_agent.go new file mode 100644 index 000000000..5b9acba18 --- /dev/null +++ b/internal/deploy/validate/local_agent.go @@ -0,0 +1,18 @@ +package validate + +import "github.com/eclipse-iofog/iofogctl/pkg/util" + +const LocalAgentConflictPort = 54321 + +func LocalAgentPortAvailable(isSystem bool) error { + if isSystem { + return nil + } + if util.IsTCPPortOpen("127.0.0.1", LocalAgentConflictPort) { + return util.NewConflictError( + "Cannot deploy LocalAgent: an agent is already running on this host (port 54321 in use). " + + "If you deployed LocalControlPlane, its systemAgent occupies this host — remove it first or deploy agents on remote hosts only.", + ) + } + return nil +} diff --git a/internal/deploy/validate/local_agent_test.go b/internal/deploy/validate/local_agent_test.go new file mode 100644 index 000000000..d8e93e3fb --- /dev/null +++ b/internal/deploy/validate/local_agent_test.go @@ -0,0 +1,39 @@ +package validate + +import ( + "net" + "testing" + + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestLocalAgentPortAvailable_SkipsForSystemAgent(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:54321") + if err != nil { + t.Skipf("cannot bind 127.0.0.1:54321 for test: %v", err) + } + defer ln.Close() + + require.NoError(t, LocalAgentPortAvailable(true)) +} + +func TestLocalAgentPortAvailable_RejectsWhenPortInUse(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:54321") + if err != nil { + t.Skipf("cannot bind 127.0.0.1:54321 for test: %v", err) + } + defer ln.Close() + + err = LocalAgentPortAvailable(false) + var conflictErr *util.ConflictError + require.ErrorAs(t, err, &conflictErr) + require.Contains(t, err.Error(), "54321") +} + +func TestLocalAgentPortAvailable_AllowsWhenPortFree(t *testing.T) { + if util.IsTCPPortOpen("127.0.0.1", LocalAgentConflictPort) { + t.Skip("port 54321 already in use on this host") + } + require.NoError(t, LocalAgentPortAvailable(false)) +} diff --git a/internal/deploy/validate/local_controlplane.go b/internal/deploy/validate/local_controlplane.go new file mode 100644 index 000000000..dd5b339be --- /dev/null +++ b/internal/deploy/validate/local_controlplane.go @@ -0,0 +1,15 @@ +package validate + +import "github.com/eclipse-iofog/iofogctl/pkg/util" + +func LocalControlPlaneDeploy(localCPCount int, namespaceHasOtherControlPlane bool) error { + if localCPCount > 1 { + return util.NewInputError("Specified multiple Local Control Planes in a single deploy file") + } + if localCPCount > 0 && namespaceHasOtherControlPlane { + return util.NewInputError( + "Namespace already has a different Control Plane kind; delete it before deploying LocalControlPlane", + ) + } + return nil +} diff --git a/internal/deploy/validate/local_controlplane_test.go b/internal/deploy/validate/local_controlplane_test.go new file mode 100644 index 000000000..c8767b61e --- /dev/null +++ b/internal/deploy/validate/local_controlplane_test.go @@ -0,0 +1,34 @@ +package validate + +import ( + "testing" + + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/stretchr/testify/require" +) + +func requireInputError(t *testing.T, err error) { + t.Helper() + var inputErr *util.InputError + require.ErrorAs(t, err, &inputErr) +} + +func TestLocalControlPlaneDeploy_AllowsInitial(t *testing.T) { + require.NoError(t, LocalControlPlaneDeploy(1, false)) +} + +func TestLocalControlPlaneDeploy_AllowsRedeployLocalControlPlane(t *testing.T) { + require.NoError(t, LocalControlPlaneDeploy(1, false)) +} + +func TestLocalControlPlaneDeploy_RejectsMultipleInFile(t *testing.T) { + err := LocalControlPlaneDeploy(2, false) + requireInputError(t, err) + require.Contains(t, err.Error(), "multiple Local Control Planes") +} + +func TestLocalControlPlaneDeploy_RejectsOtherControlPlaneKind(t *testing.T) { + err := LocalControlPlaneDeploy(1, true) + requireInputError(t, err) + require.Contains(t, err.Error(), "different Control Plane kind") +} diff --git a/internal/deploy/validate/remote_controller.go b/internal/deploy/validate/remote_controller.go new file mode 100644 index 000000000..fdb1352ef --- /dev/null +++ b/internal/deploy/validate/remote_controller.go @@ -0,0 +1,137 @@ +package validate + +import ( + "bytes" + "errors" + "fmt" + "io" + + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" +) + +type deployControllerRef struct { + name string + host string +} + +type deployDocMetadata struct { + Name string `yaml:"name"` +} + +type deployDocHeader struct { + Kind config.Kind `yaml:"kind"` + Metadata deployDocMetadata `yaml:"metadata"` + Spec map[string]interface{} `yaml:"spec"` +} + +// RemoteControllerDeploy rejects same-file ControlPlane controllers[] entries that +// collide by name or host with standalone Controller documents. +func RemoteControllerDeploy(inputFile string) error { + yamlFile, err := util.ReadUserFile(inputFile) + if err != nil { + return err + } + + var cpControllers []deployControllerRef + var standaloneControllers []deployControllerRef + + r := bytes.NewReader(yamlFile) + dec := yaml.NewDecoder(r) + + var doc deployDocHeader + decodeErr := dec.Decode(&doc) + for ; !errors.Is(decodeErr, io.EOF); decodeErr = dec.Decode(&doc) { + if decodeErr != nil { + return decodeErr + } + kind := normalizeDeployKind(doc.Kind) + switch kind { + case config.RemoteControlPlaneKind: + cpControllers = append(cpControllers, controllersFromCPSpec(doc.Spec)...) + case config.RemoteControllerKind: + standaloneControllers = append(standaloneControllers, deployControllerRef{ + name: doc.Metadata.Name, + host: stringFromMap(doc.Spec, "host"), + }) + } + doc = deployDocHeader{} + } + + for _, standalone := range standaloneControllers { + for _, cpCtrl := range cpControllers { + if standalone.name != "" && standalone.name == cpCtrl.name { + return util.NewInputError(fmt.Sprintf( + "controller name %q appears in both ControlPlane spec.controllers and standalone Controller in the same deploy file", + standalone.name, + )) + } + if standalone.host != "" && standalone.host == cpCtrl.host { + return util.NewInputError(fmt.Sprintf( + "controller host %q appears in both ControlPlane spec.controllers and standalone Controller in the same deploy file", + standalone.host, + )) + } + } + } + + return nil +} + +func controllersFromCPSpec(spec map[string]interface{}) []deployControllerRef { + raw, ok := spec["controllers"] + if !ok { + return nil + } + items, ok := raw.([]interface{}) + if !ok { + return nil + } + refs := make([]deployControllerRef, 0, len(items)) + for _, item := range items { + ctrl, ok := item.(map[interface{}]interface{}) + if !ok { + continue + } + refs = append(refs, deployControllerRef{ + name: stringFromMapAny(ctrl, "name"), + host: stringFromMapAny(ctrl, "host"), + }) + } + return refs +} + +func stringFromMap(spec map[string]interface{}, key string) string { + if spec == nil { + return "" + } + value, ok := spec[key] + if !ok { + return "" + } + str, ok := value.(string) + if !ok { + return "" + } + return str +} + +func stringFromMapAny(values map[interface{}]interface{}, key string) string { + value, ok := values[key] + if !ok { + return "" + } + str, ok := value.(string) + if !ok { + return "" + } + return str +} + +func normalizeDeployKind(kind config.Kind) config.Kind { + if kind == "RemoteController" { + return config.RemoteControllerKind + } + return kind +} diff --git a/internal/deploy/validate/remote_controller_test.go b/internal/deploy/validate/remote_controller_test.go new file mode 100644 index 000000000..fe3a93298 --- /dev/null +++ b/internal/deploy/validate/remote_controller_test.go @@ -0,0 +1,121 @@ +package validate + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func writeDeployFixture(t *testing.T, content string) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "deploy.yaml") + require.NoError(t, os.WriteFile(path, []byte(content), 0644)) + return path +} + +func TestRemoteControllerDeployNoCollision(t *testing.T) { + path := writeDeployFixture(t, `apiVersion: datasance.com/v3 +kind: ControlPlane +metadata: + name: cp +spec: + controllers: + - name: remote-1 + host: 10.0.0.1 +--- +apiVersion: datasance.com/v3 +kind: Controller +metadata: + name: remote-2 +spec: + host: 10.0.0.2 +`) + require.NoError(t, RemoteControllerDeploy(path)) +} + +func TestRemoteControllerDeployNameCollision(t *testing.T) { + path := writeDeployFixture(t, `apiVersion: datasance.com/v3 +kind: ControlPlane +metadata: + name: cp +spec: + controllers: + - name: remote-1 + host: 10.0.0.1 +--- +apiVersion: datasance.com/v3 +kind: Controller +metadata: + name: remote-1 +spec: + host: 10.0.0.2 +`) + err := RemoteControllerDeploy(path) + require.Error(t, err) + require.Contains(t, err.Error(), "name") +} + +func TestRemoteControllerDeployControlPlaneOnlyFullSpec(t *testing.T) { + path := writeDeployFixture(t, `apiVersion: datasance.com/v3 +kind: ControlPlane +metadata: + name: iofog +spec: + iofogUser: + name: Foo + email: user@domain.com + password: "TestPassword12!" + controller: + publicUrl: http://192.168.139.85:51121 + consoleUrl: http://192.168.139.85 + auth: + mode: embedded + bootstrap: + username: admin + password: "LocalTest12!" + systemMicroservices: + router: + amd64: ghcr.io/datasance/router:3.8.0-rc.1 + nats: + enabled: true + controllers: + - name: remote-1 + host: 0.0.0.0 + ssh: + user: ubuntu + keyFile: ~/.ssh/id_ed25519 + port: 32222 + systemAgent: + config: + host: 192.168.139.85 + arch: arm64 + containerEngine: edgelet + deploymentType: native +`) + require.NoError(t, RemoteControllerDeploy(path)) +} + +func TestRemoteControllerDeployHostCollision(t *testing.T) { + path := writeDeployFixture(t, `apiVersion: datasance.com/v3 +kind: ControlPlane +metadata: + name: cp +spec: + controllers: + - name: remote-1 + host: 10.0.0.1 +--- +apiVersion: datasance.com/v3 +kind: RemoteController +metadata: + name: remote-2 +spec: + host: 10.0.0.1 +`) + err := RemoteControllerDeploy(path) + require.Error(t, err) + require.Contains(t, err.Error(), "host") +} diff --git a/internal/deploy/volume/factory.go b/internal/deploy/volume/factory.go index f91523c54..3a62d7c55 100644 --- a/internal/deploy/volume/factory.go +++ b/internal/deploy/volume/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployvolume import ( diff --git a/internal/deploy/volume/local.go b/internal/deploy/volume/local.go index 9016484f8..451f453ee 100644 --- a/internal/deploy/volume/local.go +++ b/internal/deploy/volume/local.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployvolume import ( @@ -36,7 +23,7 @@ func (exe *localExecutor) Execute() error { return nil } util.SpinStart("Pushing volumes to Agents") - util.PrintNotify("Local Agent uses the host filesystem when mounting/binding volumes to the Microservices. Therefore deploying a Volume to a Local Agent is unecessary.") + util.PrintNotify("Local Agent uses the host filesystem when mounting/binding volumes to the Microservices. Therefore deploying a Volume to a Local Agent is unnecessary.") if exe.volume.Source != exe.volume.Destination { msg := `Source '%s' is different from destination '%s' This may result cause issues, as the Microservices running on the Local Agent will use the host filesystem to bind/mount volumes.` diff --git a/internal/deploy/volume/remote.go b/internal/deploy/volume/remote.go index 81d0b813d..9a8c083de 100644 --- a/internal/deploy/volume/remote.go +++ b/internal/deploy/volume/remote.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployvolume import ( @@ -59,6 +46,7 @@ func (exe *remoteExecutor) execute(agentIdx int, ch chan error) { ch <- fmt.Errorf(msg, agent.Name, err.Error()) return } + ssh.SetPort(agent.SSH.Port) if err := ssh.Connect(); err != nil { msg := "failed to Connect to Agent %s.\n%s" ch <- fmt.Errorf(msg, agent.Name, err.Error()) diff --git a/internal/deploy/volumeMount/factory.go b/internal/deploy/volumeMount/factory.go index eab80f547..2f32bd532 100644 --- a/internal/deploy/volumeMount/factory.go +++ b/internal/deploy/volumeMount/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package deployvolumemount import ( diff --git a/internal/describe/agent.go b/internal/describe/agent.go index e1cce8bb6..11eb1429f 100644 --- a/internal/describe/agent.go +++ b/internal/describe/agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/agent_config.go b/internal/describe/agent_config.go index ec02462a5..7b93882b9 100644 --- a/internal/describe/agent_config.go +++ b/internal/describe/agent_config.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/agent_platform_status_test.go b/internal/describe/agent_platform_status_test.go new file mode 100644 index 000000000..79f2afc2e --- /dev/null +++ b/internal/describe/agent_platform_status_test.go @@ -0,0 +1,49 @@ +package describe + +import ( + "testing" + "time" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/stretchr/testify/require" +) + +func TestFormatAgentStatusPlatformStatus(t *testing.T) { + lastErr := "router reconcile timeout" + transition := time.Date(2026, 6, 24, 22, 52, 59, 824000000, time.UTC) + status := rsc.AgentStatus{ + DaemonStatus: "RUNNING", + WarningMessage: "Platform reconcile: progressing", + PlatformStatus: &client.PlatformStatus{ + Phase: client.PlatformReady, + Generation: 2, + ObservedGeneration: 2, + LastError: &lastErr, + LastTransitionAt: &transition, + Conditions: []client.PlatformCondition{ + {Type: "RouterReady", Status: "True", Reason: "ReconcileComplete"}, + {Type: "NatsReady", Status: "True", Reason: "ReconcileComplete"}, + }, + }, + } + + got := FormatAgentStatus(status) + ps, ok := got["platformStatus"].(map[string]interface{}) + require.True(t, ok, "platformStatus should be present") + require.Equal(t, "Ready", ps["phase"]) + require.Equal(t, 2, ps["generation"]) + require.Equal(t, 2, ps["observedGeneration"]) + require.Equal(t, lastErr, ps["lastError"]) + require.Equal(t, transition.Format(time.RFC3339Nano), ps["lastTransitionAt"]) + + conditions, ok := ps["conditions"].([]map[string]interface{}) + require.True(t, ok) + require.Len(t, conditions, 2) + require.Equal(t, "RouterReady", conditions[0]["type"]) +} + +func TestFormatAgentStatusOmitsNilPlatformStatus(t *testing.T) { + got := FormatAgentStatus(rsc.AgentStatus{}) + require.NotContains(t, got, "platformStatus") +} diff --git a/internal/describe/agent_status_test.go b/internal/describe/agent_status_test.go new file mode 100644 index 000000000..162208bd7 --- /dev/null +++ b/internal/describe/agent_status_test.go @@ -0,0 +1,92 @@ +package describe + +import ( + "os" + "path/filepath" + "runtime" + "testing" + "time" + + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func agentStatusFixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "testdata", name) +} + +func loadAgentStatusFixture(t *testing.T, name string) map[string]interface{} { + t.Helper() + data, err := os.ReadFile(agentStatusFixturePath(name)) + require.NoError(t, err) + var expected map[string]interface{} + require.NoError(t, yaml.Unmarshal(data, &expected)) + return expected +} + +func TestFormatAgentStatusGoldenV38Fields(t *testing.T) { + expected := loadAgentStatusFixture(t, "agent-config-status-v38.yaml") + + status := rsc.AgentStatus{ + DaemonStatus: "UNKNOWN", + SecurityStatus: "OK", + WarningMessage: "HEALTHY", + SecurityViolationInfo: "No violation", + CPUUsage: 0.13, + DiskUsage: 0.00017833709716796875, // ~187 B as MiB input + MemoryViolation: "false", + DiskViolation: "false", + CPUViolation: "false", + RepositoryStatus: "[]", + IPAddress: "172.20.0.1", + IPAddressExternal: "176.234.134.248", + LastCommandTimeMsUTC: 0, + Version: "3.7.0", + IsReadyToUpgrade: false, + IsReadyToRollback: false, + Tunnel: "", + GpsStatus: "HEALTHY", + AvailableRuntimes: []string{"crun", "x-runtime", "y-runtime"}, + RuntimeAgentPhase: "", + ControlPlaneQuiesced: false, + } + + got := FormatAgentStatus(status) + + for key, want := range expected { + require.Contains(t, got, key, "missing status field %q", key) + if key == "availableRuntimes" { + require.Equal(t, []string{"crun", "x-runtime", "y-runtime"}, got[key]) + continue + } + require.Equal(t, want, got[key], "status field %q", key) + } +} + +func TestFormatAgentStatusV38FieldsAlwaysPresent(t *testing.T) { + got := FormatAgentStatus(rsc.AgentStatus{}) + + require.Contains(t, got, "availableRuntimes") + require.Contains(t, got, "runtimeAgentPhase") + require.Contains(t, got, "controlPlaneQuiesced") + require.Equal(t, "", got["runtimeAgentPhase"]) + require.Equal(t, false, got["controlPlaneQuiesced"]) +} + +func TestFormatAgentStatusUptimeAndTimestamps(t *testing.T) { + // 2026-05-07T15:21:10+03:00 in ms since epoch + tsMilli := time.Date(2026, 5, 7, 15, 21, 10, 0, time.FixedZone("TRT", 3*3600)).UnixMilli() + status := rsc.AgentStatus{ + LastActive: tsMilli, + LastStatusTimeMsUTC: tsMilli, + UptimeMs: (5*time.Hour + 3*time.Minute).Milliseconds(), + } + + got := FormatAgentStatus(status) + + require.Equal(t, "2026-05-07T15:21:10+03:00", got["lastActive"]) + require.Equal(t, "2026-05-07T15:21:10+03:00", got["lastStatusTime"]) + require.Equal(t, "5h3m", got["uptime"]) +} diff --git a/internal/describe/application.go b/internal/describe/application.go index ea3832e3c..094b1a5a0 100644 --- a/internal/describe/application.go +++ b/internal/describe/application.go @@ -1,19 +1,8 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( + "errors" + apps "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/apps" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/config" @@ -23,14 +12,14 @@ import ( ) type applicationExecutor struct { - namespace string - name string - filename string - flow *client.FlowInfo - client *client.Client - msvcs []*client.MicroserviceInfo - msvcPerID map[string]*client.MicroserviceInfo - natsCfg *client.ApplicationNatsConfig + namespace string + name string + filename string + application *client.ApplicationInfo + client *client.Client + msvcs []*client.MicroserviceInfo + msvcPerID map[string]*client.MicroserviceInfo + natsCfg *client.ApplicationNatsConfig } func newApplicationExecutor(namespace, name, filename string) *applicationExecutor { @@ -49,15 +38,16 @@ func (exe *applicationExecutor) init() (err error) { application, err := exe.client.GetApplicationByName(exe.name) // If not found error, try legacy - if _, ok := err.(*client.NotFoundError); ok { + notFoundError := &client.NotFoundError{} + if errors.As(err, ¬FoundError) { return exe.initLegacy() } // Return other errors if err != nil { return err } - // TODO: Use Application instead of flow - exe.flow = &client.FlowInfo{ + // TODO: Use Application instead of application + exe.application = &client.ApplicationInfo{ Name: application.Name, IsActivated: application.IsActivated, Description: application.Description, @@ -104,7 +94,7 @@ func (exe *applicationExecutor) Execute() error { return err } // Remove fields - yamlMsvc.Flow = nil + yamlMsvc.Application = "" yamlMsvcs = append(yamlMsvcs, *yamlMsvc) } if exe.natsCfg != nil { @@ -115,10 +105,10 @@ func (exe *applicationExecutor) Execute() error { } application := rsc.Application{ - Name: exe.flow.Name, + Name: exe.application.Name, Microservices: yamlMsvcs, NatsConfig: natsCfg, - ID: exe.flow.ID, + ID: exe.application.ID, } header := config.Header{ diff --git a/internal/describe/application_legacy.go b/internal/describe/application_legacy.go index 49c67cf6f..18a2ab18e 100644 --- a/internal/describe/application_legacy.go +++ b/internal/describe/application_legacy.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( @@ -19,11 +6,11 @@ import ( ) func (exe *applicationExecutor) initLegacy() (err error) { - exe.flow, err = exe.client.GetFlowByName(exe.name) + exe.application, err = exe.client.GetApplicationByName(exe.name) if err != nil { return } - msvcListResponse, err := exe.client.GetMicroservicesPerFlow(exe.flow.ID) + msvcListResponse, err := exe.client.GetMicroservicesByApplication(exe.application.Name) if err != nil { return } diff --git a/internal/describe/certificate.go b/internal/describe/certificate.go index 637189ec0..44fc8bb2c 100644 --- a/internal/describe/certificate.go +++ b/internal/describe/certificate.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/config_map.go b/internal/describe/config_map.go index 8ac1c5ac5..d6f67be04 100644 --- a/internal/describe/config_map.go +++ b/internal/describe/config_map.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( @@ -78,7 +65,7 @@ func printConfigMapWithLiteralStrings(header config.Header, writer io.Writer) er for key, value := range dataMap { if strings.Contains(value, "\n") { // Use literal block scalar for multi-line strings - _, err = writer.Write([]byte(fmt.Sprintf(" %s: |\n", key))) + _, err = fmt.Fprintf(writer, " %s: |\n", key) if err != nil { return err } @@ -86,14 +73,14 @@ func printConfigMapWithLiteralStrings(header config.Header, writer io.Writer) er // Split by newlines and add proper indentation lines := strings.Split(value, "\n") for _, line := range lines { - _, err = writer.Write([]byte(fmt.Sprintf(" %s\n", line))) + _, err = fmt.Fprintf(writer, " %s\n", line) if err != nil { return err } } } else { // Regular string - _, err = writer.Write([]byte(fmt.Sprintf(" %s: %s\n", key, value))) + _, err = fmt.Fprintf(writer, " %s: %s\n", key, value) if err != nil { return err } diff --git a/internal/describe/controller.go b/internal/describe/controller.go index f28fde81d..9a49ed292 100644 --- a/internal/describe/controller.go +++ b/internal/describe/controller.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/controlplane.go b/internal/describe/controlplane.go index 9ba895065..08751221a 100644 --- a/internal/describe/controlplane.go +++ b/internal/describe/controlplane.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/edge_resource.go b/internal/describe/edge_resource.go deleted file mode 100644 index b164a7d81..000000000 --- a/internal/describe/edge_resource.go +++ /dev/null @@ -1,93 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package describe - -import ( - "fmt" - - "github.com/eclipse-iofog/iofogctl/internal/config" - rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -type edgeResourceExecutor struct { - namespace string - name string - version string - filename string -} - -func newEdgeResourceExecutor(namespace, name, version, filename string) *edgeResourceExecutor { - return &edgeResourceExecutor{ - namespace: namespace, - name: name, - version: version, - filename: filename, - } -} - -func (exe *edgeResourceExecutor) GetName() string { - return fmt.Sprintf("%s/%s", exe.name, exe.version) -} - -func (exe *edgeResourceExecutor) Execute() error { - _, err := config.GetNamespace(exe.namespace) - if err != nil { - return err - } - - // Connect to Controller - clt, err := clientutil.NewControllerClient(exe.namespace) - if err != nil { - return err - } - - // Get Edge Resource - edge, err := clt.GetHTTPEdgeResourceByName(exe.name, exe.version) - if err != nil { - return err - } - - // Convert to YAML - header := config.Header{ - APIVersion: config.LatestAPIVersion, - Kind: config.EdgeResourceKind, - Metadata: config.HeaderMetadata{ - Namespace: exe.namespace, - Name: exe.name, - }, - Spec: rsc.EdgeResource{ - Description: edge.Description, - Display: edge.Display, - Interface: &edge.Interface, - InterfaceProtocol: edge.InterfaceProtocol, - Name: edge.Name, - OrchestrationTags: edge.OrchestrationTags, - Version: edge.Version, - Custom: edge.Custom, - }, - } - - if exe.filename == "" { - if err := util.Print(header); err != nil { - return err - } - } else { - if err := util.FPrint(header, exe.filename); err != nil { - return err - } - } - return nil -} diff --git a/internal/describe/factory.go b/internal/describe/factory.go index a823efedf..bd9b9d924 100644 --- a/internal/describe/factory.go +++ b/internal/describe/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( @@ -53,8 +40,6 @@ func NewExecutor(opt *Options) (execute.Executor, error) { return newApplicationExecutor(opt.Namespace, opt.Name, opt.Filename), nil case "volume": return newVolumeExecutor(opt.Namespace, opt.Name, opt.Filename), nil - case "edge-resource": - return newEdgeResourceExecutor(opt.Namespace, opt.Name, opt.Version, opt.Filename), nil case "secret": return newSecretExecutor(opt.Namespace, opt.Name, opt.Filename), nil case "configmap": diff --git a/internal/describe/microservice.go b/internal/describe/microservice.go index 0479a7b13..ef2394bdc 100644 --- a/internal/describe/microservice.go +++ b/internal/describe/microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/namespace.go b/internal/describe/namespace.go index 53580d567..202681023 100644 --- a/internal/describe/namespace.go +++ b/internal/describe/namespace.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/registry.go b/internal/describe/registry.go index 398cb8ae3..a50481857 100644 --- a/internal/describe/registry.go +++ b/internal/describe/registry.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/role.go b/internal/describe/role.go index 6fb5b4aa6..aabddca64 100644 --- a/internal/describe/role.go +++ b/internal/describe/role.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/rolebinding.go b/internal/describe/rolebinding.go index e42e3cb3b..40b14f04f 100644 --- a/internal/describe/rolebinding.go +++ b/internal/describe/rolebinding.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/secret.go b/internal/describe/secret.go index 4ca75b4ba..94f852287 100644 --- a/internal/describe/secret.go +++ b/internal/describe/secret.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/service.go b/internal/describe/service.go index cf22cdf97..e3b525afa 100644 --- a/internal/describe/service.go +++ b/internal/describe/service.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( @@ -20,6 +7,13 @@ import ( "github.com/eclipse-iofog/iofogctl/pkg/util" ) +func stringPtrValue(s *string) string { + if s == nil { + return "" + } + return *s +} + type serviceExecutor struct { namespace string name string @@ -71,7 +65,7 @@ func (exe *serviceExecutor) Execute() error { TargetPort: service.TargetPort, BridgePort: service.BridgePort, DefaultBridge: service.DefaultBridge, - K8sType: service.K8sType, + K8sType: stringPtrValue(service.K8sType), ServiceEndpoint: service.ServiceEndpoint, ServicePort: service.ServicePort, }, diff --git a/internal/describe/serviceaccount.go b/internal/describe/serviceaccount.go index 80f98be46..2a1ff99c9 100644 --- a/internal/describe/serviceaccount.go +++ b/internal/describe/serviceaccount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/system_microservice.go b/internal/describe/system_microservice.go index 50f1aa81f..34ccf5ac4 100644 --- a/internal/describe/system_microservice.go +++ b/internal/describe/system_microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/template.go b/internal/describe/template.go index e83b1631e..8f43d0574 100644 --- a/internal/describe/template.go +++ b/internal/describe/template.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/testdata/agent-config-status-v38.yaml b/internal/describe/testdata/agent-config-status-v38.yaml new file mode 100644 index 000000000..df69d2ed2 --- /dev/null +++ b/internal/describe/testdata/agent-config-status-v38.yaml @@ -0,0 +1,25 @@ +# Golden status excerpt from describe-agent-config-output.yaml (v3.8 fields) +availableRuntimes: + - crun + - x-runtime + - y-runtime +runtimeAgentPhase: "" +controlPlaneQuiesced: false +daemonStatus: UNKNOWN +securityStatus: OK +warningMessage: HEALTHY +securityViolationInfo: No violation +cpuUsage: 0.13 % +cpuViolation: "false" +diskUsage: 187 B +diskViolation: "false" +gpsStatus: HEALTHY +ipAddress: 172.20.0.1 +ipAddressExternal: 176.234.134.248 +isReadyToRollback: false +isReadyToUpgrade: false +lastCommandTime: Never +memoryViolation: "false" +repositoryStatus: '[]' +tunnel: "" +version: 3.7.0 diff --git a/internal/describe/utils.go b/internal/describe/utils.go index 3e44889af..c18f757b4 100644 --- a/internal/describe/utils.go +++ b/internal/describe/utils.go @@ -1,19 +1,7 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( + "errors" "fmt" "time" @@ -34,7 +22,8 @@ func MapClientMicroserviceToDeployMicroservice(msvc *client.MicroserviceInfo, cl if msvc.CatalogItemID != 0 { catalogItem, err = clt.GetCatalogItem(msvc.CatalogItemID) if err != nil { - if httpErr, ok := err.(*client.HTTPError); ok && httpErr.Code == 404 { + httpErr := &client.HTTPError{} + if errors.As(err, &httpErr) { catalogItem = nil } else { return nil, nil, nil, err @@ -43,16 +32,16 @@ func MapClientMicroserviceToDeployMicroservice(msvc *client.MicroserviceInfo, cl } applicationName := msvc.Application - if msvc.Application == "" { - if msvc.FlowID > 0 { - // Legacy - flow, err := clt.GetFlowByID(msvc.FlowID) - if err != nil { - return nil, nil, nil, err - } - applicationName = flow.Name - } - } + // if msvc.Application == "" { + // if msvc.ApplicationName != "" { + // // Legacy + // application, err := clt.GetApplicationByName(msvc.ApplicationName) + // if err != nil { + // return nil, nil, nil, err + // } + // applicationName = application.Name + // } + // } return constructMicroservice(msvc, agent.Name, applicationName, catalogItem) } @@ -131,7 +120,7 @@ func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName msvc.Agent = apps.MicroserviceAgent{ Name: agentName, } - var armImage, x86Image string + var armImage, amd64Image, riscv64Image, arm64Image string var msvcImages []client.CatalogImage if catalogItem != nil { msvcImages = catalogItem.Images @@ -139,9 +128,13 @@ func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName msvcImages = msvcInfo.Images } for _, image := range msvcImages { - switch client.AgentTypeIDAgentTypeDict[image.AgentTypeID] { - case "x86": - x86Image = image.ContainerImage + switch client.ArchIDToName[image.ArchID] { + case "amd64": + amd64Image = image.ContainerImage + case "arm64": + armImage = image.ContainerImage + case "riscv64": + riscv64Image = image.ContainerImage case "arm": armImage = image.ContainerImage default: @@ -158,14 +151,21 @@ func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName } images := apps.MicroserviceImages{ CatalogID: msvcInfo.CatalogItemID, - X86: x86Image, + AMD64: amd64Image, + ARM64: arm64Image, + RISCV64: riscv64Image, ARM: armImage, Registry: client.RegistryTypeIDRegistryTypeDict[registryID], } for _, img := range imgArray { - if img.AgentTypeID == 1 { - images.X86 = img.ContainerImage - } else if img.AgentTypeID == 2 { + switch img.ArchID { + case 1: + images.AMD64 = img.ContainerImage + case 2: + images.ARM64 = img.ContainerImage + case 3: + images.RISCV64 = img.ContainerImage + case 4: images.ARM = img.ContainerImage } } @@ -225,7 +225,7 @@ func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName msvc.Container.Volumes = &volumes msvc.Container.Env = &envs msvc.Container.ExtraHosts = &extraHosts - msvc.Container.CpuSetCpus = msvcInfo.CpuSetCpus + msvc.Container.CPUSetCpus = msvcInfo.CPUSetCpus msvc.Container.MemoryLimit = &msvcInfo.MemoryLimit if hasHealthCheck { msvc.Container.HealthCheck = &healthCheck @@ -237,7 +237,7 @@ func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName } } msvc.Schedule = msvcInfo.Schedule - msvc.Application = &appName + msvc.Application = appName status = new(apps.MicroserviceStatusInfo) status.Status = msvcInfo.Status.Status @@ -309,7 +309,13 @@ func FormatAgentStatus(status rsc.AgentStatus) map[string]interface{} { formatted["daemonStatus"] = status.DaemonStatus formatted["securityStatus"] = status.SecurityStatus formatted["warningMessage"] = status.WarningMessage + if status.PlatformStatus != nil { + formatted["platformStatus"] = formatPlatformStatus(status.PlatformStatus) + } formatted["securityViolationInfo"] = status.SecurityViolationInfo + formatted["availableRuntimes"] = status.AvailableRuntimes + formatted["runtimeAgentPhase"] = status.RuntimeAgentPhase + formatted["controlPlaneQuiesced"] = status.ControlPlaneQuiesced // Format timestamps if status.LastActive > 0 { @@ -393,6 +399,43 @@ func FormatAgentStatus(status rsc.AgentStatus) map[string]interface{} { return formatted } +func formatPlatformStatus(ps *client.PlatformStatus) map[string]interface{} { + if ps == nil { + return nil + } + out := map[string]interface{}{ + "phase": string(ps.Phase), + "generation": ps.Generation, + "observedGeneration": ps.ObservedGeneration, + } + if ps.LastError != nil { + out["lastError"] = *ps.LastError + } else { + out["lastError"] = nil + } + if ps.LastTransitionAt != nil { + out["lastTransitionAt"] = ps.LastTransitionAt.Format(time.RFC3339Nano) + } + if len(ps.Conditions) > 0 { + conditions := make([]map[string]interface{}, len(ps.Conditions)) + for i, c := range ps.Conditions { + cond := map[string]interface{}{ + "type": c.Type, + "status": c.Status, + } + if c.Reason != "" { + cond["reason"] = c.Reason + } + if c.Message != "" { + cond["message"] = c.Message + } + conditions[i] = cond + } + out["conditions"] = conditions + } + return out +} + // formatBytesAuto formats bytes with automatic unit scaling (B, KB, MB, GB, etc.) func formatBytesAuto(bytes float64) string { const unit = 1024 diff --git a/internal/describe/volume.go b/internal/describe/volume.go index 8b59ac22e..e861f3804 100644 --- a/internal/describe/volume.go +++ b/internal/describe/volume.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/describe/volume_mount.go b/internal/describe/volume_mount.go index 4604f3292..584f13102 100644 --- a/internal/describe/volume_mount.go +++ b/internal/describe/volume_mount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package describe import ( diff --git a/internal/detach/agent/execute.go b/internal/detach/agent/execute.go index f074650ab..8a1315b3d 100644 --- a/internal/detach/agent/execute.go +++ b/internal/detach/agent/execute.go @@ -1,22 +1,10 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package detachagent import ( "fmt" "github.com/eclipse-iofog/iofogctl/internal/config" + deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" "github.com/eclipse-iofog/iofogctl/internal/execute" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" @@ -42,10 +30,10 @@ func (exe executor) Execute() error { // Check doesn't already exist with same name if _, err := config.GetDetachedAgent(exe.name); err == nil { - msg := `An Agent with the name '%s' is already detached. Rename one of the Agents and try to detach again: -iofogctl rename agent %s %s-2 -n %s -iofogctl rename agent %s %s-2 -n %s --detached` - return util.NewConflictError(fmt.Sprintf(msg, exe.name, exe.name, exe.name, exe.namespace, exe.name, exe.name, exe.namespace)) + return util.NewConflictError(fmt.Sprintf( + "An Agent with the name '%s' is already detached. Detach or delete the existing detached Agent before trying again.", + exe.name, + )) } ns, err := config.GetNamespace(exe.namespace) @@ -85,7 +73,8 @@ iofogctl rename agent %s %s-2 -n %s --detached` // Deprovision agent switch agent := baseAgent.(type) { case *rsc.LocalAgent: - if err := exe.localDeprovision(); err != nil { + cfg := deployairgap.EdgeletInstallConfig(deployairgap.LocalEdgeletHostOS(), agent.Config, agent.Package) + if err := exe.localDeprovision(agent.Name, agent.UUID, cfg); err != nil { return err } case *rsc.RemoteAgent: diff --git a/internal/detach/agent/local.go b/internal/detach/agent/local.go index 03ae30689..0ae6bcb00 100644 --- a/internal/detach/agent/local.go +++ b/internal/detach/agent/local.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package detachagent import ( @@ -20,16 +7,17 @@ import ( "github.com/eclipse-iofog/iofogctl/pkg/util" ) -func (exe executor) localDeprovision() error { - containerClient, err := install.NewLocalContainerClient() +func (exe executor) localDeprovision(agentName, agentUUID string, cfg install.EdgeletInstallConfig) error { + edgelet, err := install.NewLocalEdgelet(agentName, agentUUID, cfg) if err != nil { - util.PrintNotify(fmt.Sprintf("Could not deprovision local iofog-agent container. Error: %s\n", err.Error())) - } else if _, err = containerClient.ExecuteCmd(install.GetLocalContainerName("agent", false), []string{ - "sudo", - "iofog-agent", - "deprovision", - }); err != nil { - util.PrintNotify(fmt.Sprintf("Could not deprovision local iofog-agent container. Error: %s\n", err.Error())) + util.PrintNotify(fmt.Sprintf("Could not deprovision local edgelet. Error: %s\n", err.Error())) + return nil + } + if err := edgelet.Deprovision(); err != nil { + util.PrintNotify(fmt.Sprintf("Could not deprovision local edgelet. Error: %s\n", err.Error())) + } + if err := edgelet.Uninstall(false); err != nil { + util.PrintNotify(fmt.Sprintf("Could not uninstall local edgelet. Error: %s\n", err.Error())) } return nil } diff --git a/internal/detach/agent/remote.go b/internal/detach/agent/remote.go index 37e242866..ba14f6e50 100644 --- a/internal/detach/agent/remote.go +++ b/internal/detach/agent/remote.go @@ -1,21 +1,9 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package detachagent import ( "fmt" + deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" @@ -24,20 +12,27 @@ import ( func (exe executor) remoteDeprovision(agent *rsc.RemoteAgent) error { if agent.ValidateSSH() != nil { util.PrintNotify("Could not deprovision daemon for Agent " + agent.Name + ". SSH details missing from local configuration. Use configure command to add SSH details.") - } else { - sshAgent, err := install.NewRemoteAgent( - agent.SSH.User, - agent.Host, - agent.SSH.Port, - agent.SSH.KeyFile, - agent.Name, - agent.UUID) - if err != nil { - return err - } - if err := sshAgent.Deprovision(); err != nil { - util.PrintNotify(fmt.Sprintf("Failed to deprovision daemon on Agent %s. %s", agent.Name, err.Error())) - } + return nil + } + + cfg := deployairgap.EdgeletInstallConfig("linux", agent.Config, agent.Package) + edgelet, err := install.NewRemoteEdgelet( + agent.SSH.User, + agent.Host, + agent.SSH.Port, + agent.SSH.KeyFile, + agent.Name, + agent.UUID, + cfg, + ) + if err != nil { + return err + } + if err := edgelet.Deprovision(); err != nil { + util.PrintNotify(fmt.Sprintf("Failed to deprovision daemon on Agent %s. %s", agent.Name, err.Error())) + } + if err := edgelet.Uninstall(false); err != nil { + util.PrintNotify(fmt.Sprintf("Failed to uninstall edgelet on Agent %s. %s", agent.Name, err.Error())) } return nil } diff --git a/internal/detach/edgeresource/execute.go b/internal/detach/edgeresource/execute.go deleted file mode 100644 index b2f218d6a..000000000 --- a/internal/detach/edgeresource/execute.go +++ /dev/null @@ -1,69 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package detachedgeresource - -import ( - "fmt" - - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/internal/execute" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -type executor struct { - name string - version string - namespace string - agent string -} - -func NewExecutor(namespace, name, version, agent string) execute.Executor { - return executor{name: name, - version: version, - namespace: namespace, - agent: agent} -} - -func (exe executor) GetName() string { - return fmt.Sprintf("%s/%s", exe.name, exe.version) -} - -func (exe executor) Execute() error { - util.SpinStart("Detaching Edge Resource") - - // Init client - clt, err := clientutil.NewControllerClient(exe.namespace) - if err != nil { - return err - } - - // Get Agent UUID - // agentInfo, err := clt.GetAgentByName(exe.agent, false) - agentInfo, err := clt.GetAgentByName(exe.agent) - if err != nil { - return err - } - // Detach from agent - req := client.LinkEdgeResourceRequest{ - AgentUUID: agentInfo.UUID, - EdgeResourceName: exe.name, - EdgeResourceVersion: exe.version, - } - if err := clt.UnlinkEdgeResource(req); err != nil { - return err - } - - return nil -} diff --git a/internal/detach/exec/agent/execute.go b/internal/detach/exec/agent/execute.go index 1bc4aadd2..e46d45dc0 100644 --- a/internal/detach/exec/agent/execute.go +++ b/internal/detach/exec/agent/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package detachexecagent import ( @@ -44,7 +31,7 @@ func (exe *executor) GetName() string { } func (exe *executor) Execute() error { - util.SpinStart("Detaching Exec Session to Agent") + util.SpinStart("Removing debug exec from Agent") // Init client clt, err := clientutil.NewControllerClient(exe.namespace) @@ -54,8 +41,7 @@ func (exe *executor) Execute() error { agent, err := clt.GetAgentByName(exe.name) if err != nil { - msg := "%s\nFailed to get Agent by name: %s" - return fmt.Errorf(msg, err.Error()) + return fmt.Errorf("failed to get Agent by name: %w", err) } req := client.DetachExecFromAgentRequest{ @@ -63,8 +49,7 @@ func (exe *executor) Execute() error { } err = clt.DetachExecFromAgent(&req) if err != nil { - msg := "%s\nFailed to detach Exec Session from Agent: %s" - return fmt.Errorf(msg, err.Error()) + return fmt.Errorf("failed to detach Exec Session from Agent: %w", err) } return nil diff --git a/internal/detach/exec/microservice/execute.go b/internal/detach/exec/microservice/execute.go deleted file mode 100644 index 152b2d26e..000000000 --- a/internal/detach/exec/microservice/execute.go +++ /dev/null @@ -1,95 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package detachexecmicroservice - -import ( - "strings" - - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/internal/execute" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -type Options struct { - Name string - Namespace string - Msvc *client.MicroserviceInfo -} - -type executor struct { - name string - namespace string - msvc *client.MicroserviceInfo -} - -func NewExecutor(opt Options) execute.Executor { - return &executor{ - name: opt.Name, - namespace: opt.Namespace, - msvc: opt.Msvc, - } -} - -func (exe *executor) GetName() string { - return exe.name -} - -func (exe *executor) Execute() error { - util.SpinStart("Detaching Exec Session to Microservice") - - // Init client - clt, err := clientutil.NewControllerClient(exe.namespace) - if err != nil { - return err - } - - appName, msvcName, err := clientutil.ParseFQName(exe.name, "Microservice") - if err != nil { - return err - } - - exe.msvc, err = clt.GetMicroserviceByName(appName, msvcName) - isSystem := false - if err != nil { - // Check if error indicates application not found - if strings.Contains(err.Error(), "Invalid application id") { - // Try system application - exe.msvc, err = clt.GetSystemMicroserviceByName(appName, msvcName) - if err != nil { - return err - } - isSystem = true - } else { - // Return other types of errors - return err - } - } - - // Attach Exec Session to Microservice - req := client.DetachExecMicroserviceRequest{ - UUID: exe.msvc.UUID, - } - if isSystem { - if err := clt.DetachExecSystemMicroservice(&req); err != nil { - return err - } - } else { - if err := clt.DetachExecMicroservice(&req); err != nil { - return err - } - } - - return nil -} diff --git a/internal/detach/volumemount/execute.go b/internal/detach/volumemount/execute.go index b4e8ca2f2..5722fd730 100644 --- a/internal/detach/volumemount/execute.go +++ b/internal/detach/volumemount/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package detachvolumemount import ( diff --git a/internal/disconnect/disconnect.go b/internal/disconnect/disconnect.go index a56541214..031555fd0 100644 --- a/internal/disconnect/disconnect.go +++ b/internal/disconnect/disconnect.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package disconnect import ( diff --git a/internal/exec/agent.go b/internal/exec/agent.go index ab0d0ba36..181774f53 100644 --- a/internal/exec/agent.go +++ b/internal/exec/agent.go @@ -1,42 +1,21 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package exec import ( - "fmt" - "net/http" - "strings" - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/internal/util/terminal" - "github.com/eclipse-iofog/iofogctl/internal/util/websocket" - "github.com/eclipse-iofog/iofogctl/pkg/util" ) type agentExecutor struct { - namespace string - name string - client *client.Client - msvc *client.MicroserviceInfo + namespace string + name string + debugImage *string } -func newAgentExecutor(namespace, name string) *agentExecutor { - a := &agentExecutor{} - a.namespace = namespace - a.name = name - return a +func newAgentExecutor(namespace, name string, debugImage *string) *agentExecutor { + return &agentExecutor{ + namespace: namespace, + name: name, + debugImage: debugImage, + } } func (exe *agentExecutor) GetName() string { @@ -44,84 +23,12 @@ func (exe *agentExecutor) GetName() string { } func (exe *agentExecutor) Execute() error { - util.SpinStart("Connecting Exec Session to Agent") - - // Init client - clt, err := clientutil.NewControllerClient(exe.namespace) - if err != nil { - return err - } - - agent, err := clt.GetAgentByName(exe.name) - if err != nil { - msg := "%s\nFailed to get Agent by name: %s" - return fmt.Errorf(msg, err.Error()) - } - - appName := fmt.Sprintf("system-%s", agent.Name) - msvcName := fmt.Sprintf("debug-%s", agent.Name) - - exe.msvc, err = clt.GetMicroserviceByName(appName, msvcName) - if err != nil { - // Check if error indicates application not found - if strings.Contains(err.Error(), "Invalid application id") { - // Try system application - exe.msvc, err = clt.GetSystemMicroserviceByName(appName, msvcName) - if err != nil { - return err - } - } else { - // Return other types of errors - return err + label := "Agent " + exe.name + return runExecSession(exe.namespace, label, func(clt *client.Client) (*client.ExecSession, error) { + msvc, err := ensureDebugExecReady(clt, exe.name, exe.debugImage) + if err != nil { + return nil, err } - } - - // Create WebSocket client - wsClient := websocket.NewClient(exe.msvc.UUID) - - // Get controller endpoint - controllerURL := clt.GetBaseURL() - // Convert http(s):// to ws(s):// - wsURL := strings.Replace(controllerURL, "http://", "ws://", 1) - wsURL = strings.Replace(wsURL, "https://", "wss://", 1) - wsURL = fmt.Sprintf("%s/microservices/system/exec/%s", wsURL, exe.msvc.UUID) - - // Set up headers - headers := http.Header{} - headers.Set("Authorization", fmt.Sprintf("Bearer %s", clt.GetAccessToken())) - util.SpinHandlePrompt() - // Connect to WebSocket - if err := wsClient.Connect(wsURL, headers); err != nil { - util.SpinHandlePromptComplete() - return util.NewError(fmt.Sprintf("failed to connect to WebSocket: %v", err)) - } - - // Create and start terminal - term := terminal.NewTerminal(wsClient) - - // Check for initial connection error - if err := wsClient.GetError(); err != nil { - util.SpinHandlePromptComplete() - formattedErr := formatWebSocketError(err) - return util.NewError(formattedErr) - } - - if err := term.Start(); err != nil { - util.SpinHandlePromptComplete() - formattedErr := formatWebSocketError(err) - return util.NewError(formattedErr) - } - - // Wait for terminal to finish - <-wsClient.GetDone() - msg := fmt.Sprintf("Successfully closed Agent %s Exec Session", exe.name) - util.PrintSuccess(msg) - - // Check if there was an error - if err := wsClient.GetError(); err != nil { - formattedErr := formatWebSocketError(err) - return util.NewError(formattedErr) - } - - return nil + return dialExecForMicroservice(clt, msvc, true) + }) } diff --git a/internal/exec/debug.go b/internal/exec/debug.go new file mode 100644 index 000000000..ac56bcc91 --- /dev/null +++ b/internal/exec/debug.go @@ -0,0 +1,147 @@ +package exec + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const ( + debugPollInterval = 2 * time.Second + debugPollMaxAttempts = 60 +) + +func isDebugMicroserviceName(name, agentName, agentUUID string) bool { + switch name { + case "debug", "debug-" + agentName, "debug-" + agentUUID: + return true + default: + return false + } +} + +func isAgentRunning(agent *client.AgentInfo) bool { + return strings.EqualFold(agent.DaemonStatus, "RUNNING") +} + +func isMicroserviceRunning(msvc *client.MicroserviceInfo) bool { + return strings.EqualFold(msvc.Status.Status, "RUNNING") +} + +func findDebugMicroservice(clt *client.Client, agent *client.AgentInfo) (*client.MicroserviceInfo, error) { + appName := "system-" + agent.Name + list, err := clt.GetSystemMicroservicesByApplication(appName) + if err != nil { + if isMissingApplicationError(err) { + return nil, nil + } + return nil, err + } + + for i := range list.Microservices { + msvc := &list.Microservices[i] + if msvc.AgentUUID != agent.UUID { + continue + } + if isDebugMicroserviceName(msvc.Name, agent.Name, agent.UUID) { + return msvc, nil + } + } + + return nil, nil +} + +func isMissingApplicationError(err error) bool { + var notFound *client.NotFoundError + if errors.As(err, ¬Found) { + return true + } + return strings.Contains(err.Error(), "Invalid application id") +} + +func attachDebugExec(clt *client.Client, agent *client.AgentInfo, image *string) error { + err := clt.AttachExecToAgent(&client.AttachExecToAgentRequest{ + UUID: agent.UUID, + Image: image, + }) + if err == nil { + return nil + } + if isDebugExecAlreadyProvisioned(err) { + return nil + } + return fmt.Errorf("failed to provision fog debug exec: %w", err) +} + +func isDebugExecAlreadyProvisioned(err error) bool { + var conflict *client.ConflictError + if errors.As(err, &conflict) { + return true + } + errStr := strings.ToLower(err.Error()) + return strings.Contains(errStr, "already") || + strings.Contains(errStr, "conflict") || + strings.Contains(errStr, "exists") +} + +func ensureDebugExecReady(clt *client.Client, agentName string, image *string) (*client.MicroserviceInfo, error) { + agent, err := clt.GetAgentByName(agentName) + if err != nil { + return nil, err + } + if !isAgentRunning(agent) { + return nil, util.NewError(ErrMsgAgentNotRunning) + } + + msvc, err := findDebugMicroservice(clt, agent) + if err != nil { + return nil, err + } + + switch { + case msvc == nil: + util.PrintNotify(fmt.Sprintf( + "Debug microservice not found. Provisioning fog debug exec for Agent %s...", + agentName, + )) + if err := attachDebugExec(clt, agent, image); err != nil { + return nil, err + } + util.PrintNotify("Waiting for debug container to start...") + case !isMicroserviceRunning(msvc): + util.PrintNotify("Waiting for debug container to start...") + default: + return msvc, nil + } + + return waitForDebugMicroserviceRunning(clt, agent) +} + +func waitForDebugMicroserviceRunning(clt *client.Client, agent *client.AgentInfo) (*client.MicroserviceInfo, error) { + startingNotified := false + + for attempt := 0; attempt < debugPollMaxAttempts; attempt++ { + msvc, err := findDebugMicroservice(clt, agent) + if err != nil { + return nil, err + } + if msvc != nil { + if isMicroserviceRunning(msvc) { + util.PrintNotify("Debug container is running. Connecting to terminal...") + return msvc, nil + } + if !startingNotified { + util.PrintNotify("Debug container is starting...") + startingNotified = true + } + } + + time.Sleep(debugPollInterval) + } + + return nil, util.NewError(ErrMsgDebugContainerTimeout) +} diff --git a/internal/exec/debug_test.go b/internal/exec/debug_test.go new file mode 100644 index 000000000..10fccadc7 --- /dev/null +++ b/internal/exec/debug_test.go @@ -0,0 +1,48 @@ +package exec + +import ( + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" +) + +func TestIsDebugMicroserviceName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + msvcName string + agentName string + agentUUID string + want bool + }{ + {name: "canonical debug", msvcName: "debug", agentName: "remote-1", agentUUID: "node-remote-1", want: true}, + {name: "legacy debug uuid", msvcName: "debug-node-legacy", agentName: "legacy-agent", agentUUID: "node-legacy", want: true}, + {name: "debug agent name", msvcName: "debug-edge-1", agentName: "edge-1", agentUUID: "uuid-1", want: true}, + {name: "router", msvcName: "router", agentName: "remote-1", agentUUID: "node-remote-1", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := isDebugMicroserviceName(tt.msvcName, tt.agentName, tt.agentUUID); got != tt.want { + t.Fatalf("isDebugMicroserviceName(%q) = %v, want %v", tt.msvcName, got, tt.want) + } + }) + } +} + +func TestIsDebugExecAlreadyProvisioned(t *testing.T) { + t.Parallel() + + if !isDebugExecAlreadyProvisioned(client.NewConflictError("debug exec already exists")) { + t.Fatal("expected conflict attach error to be treated as already provisioned") + } + if isDebugExecAlreadyProvisioned(errString("permission denied")) { + t.Fatal("expected unrelated error to remain fatal") + } +} + +type errString string + +func (e errString) Error() string { return string(e) } diff --git a/internal/exec/factory.go b/internal/exec/factory.go index f77c75889..866fc2140 100644 --- a/internal/exec/factory.go +++ b/internal/exec/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package exec import ( @@ -21,9 +8,10 @@ import ( ) type Options struct { - Resource string - Name string - Namespace string + Resource string + Name string + Namespace string + DebugImage *string } func NewExecutor(opt *Options) (execute.Executor, error) { @@ -31,7 +19,7 @@ func NewExecutor(opt *Options) (execute.Executor, error) { case "microservice": return newMicroserviceExecutor(opt.Namespace, opt.Name), nil case "agent": - return newAgentExecutor(opt.Namespace, opt.Name), nil + return newAgentExecutor(opt.Namespace, opt.Name, opt.DebugImage), nil default: return nil, util.NewInputError(fmt.Sprintf("Unknown resources: %s", opt.Resource)) } diff --git a/internal/exec/interactive_terminal.go b/internal/exec/interactive_terminal.go new file mode 100644 index 000000000..0c651ab1a --- /dev/null +++ b/internal/exec/interactive_terminal.go @@ -0,0 +1,160 @@ +package exec + +import ( + "context" + "strings" + "sync" + "time" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type interactiveTerminal struct { + session *client.ExecSession + ctx context.Context + cancel context.CancelFunc + cleanupOnce sync.Once + stdoutMutex sync.Mutex + lastCtrlCTime time.Time + lineBuffer string + userInitiatedClose bool + userInitiatedCloseM sync.Mutex +} + +func newInteractiveTerminal(session *client.ExecSession) *interactiveTerminal { + ctx, cancel := context.WithCancel(context.Background()) + return &interactiveTerminal{ + session: session, + ctx: ctx, + cancel: cancel, + } +} + +func (t *interactiveTerminal) writeToStdout(data []byte) { + t.stdoutMutex.Lock() + defer t.stdoutMutex.Unlock() + util.WriteStdout(data) +} + +func (t *interactiveTerminal) cleanup() { + t.cleanupOnce.Do(func() { + if t.session != nil { + _ = t.session.Close() + } + }) +} + +func (t *interactiveTerminal) setUserInitiatedClose() { + t.userInitiatedCloseM.Lock() + t.userInitiatedClose = true + t.userInitiatedCloseM.Unlock() +} + +func (t *interactiveTerminal) isUserInitiatedClose() bool { + t.userInitiatedCloseM.Lock() + defer t.userInitiatedCloseM.Unlock() + return t.userInitiatedClose +} + +func (t *interactiveTerminal) sendExecClose() { + t.setUserInitiatedClose() + t.cancel() +} + +func (t *interactiveTerminal) handleInput(data []byte) bool { + if len(data) == 0 { + return false + } + + switch rune(data[0]) { + case 0x03: // Ctrl+C + if time.Since(t.lastCtrlCTime) < time.Second { + t.setUserInitiatedClose() + t.cancel() + t.cleanup() + t.writeToStdout([]byte("\nExiting...\n")) + return true + } + t.lastCtrlCTime = time.Now() + t.writeToStdout([]byte("^C\n")) + } + + if err := t.session.WriteStdin(data); err != nil { + t.cancel() + return true + } + + input := string(data) + t.lineBuffer += input + if isShellExitInput(input, t.lineBuffer) { + t.lineBuffer = "" + t.sendExecClose() + t.cancel() + return true + } + if strings.ContainsAny(input, "\r\n") { + t.lineBuffer = "" + } + + return false +} + +func (t *interactiveTerminal) shouldPrintFrame(frame *client.ExecFrame) bool { + switch frame.Type { + case client.ExecMessageStdout, client.ExecMessageStderr: + return true + case client.ExecMessageActivation, client.ExecMessageControl, client.ExecMessageClose: + return false + default: + return false + } +} + +func (t *interactiveTerminal) runPlatform(startInput func()) error { + defer t.cleanup() + + errCh := make(chan error, 1) + go func() { + for { + select { + case <-t.ctx.Done(): + return + default: + frame, err := t.session.Read() + if err != nil { + if isNormalExecClose(err, t.isUserInitiatedClose()) { + t.cancel() + return + } + errCh <- err + t.cancel() + return + } + if frame == nil { + t.cancel() + return + } + if frame.Type == client.ExecMessageClose { + t.cancel() + return + } + if t.shouldPrintFrame(frame) && len(frame.Data) > 0 { + t.writeToStdout(frame.Data) + } + } + } + }() + + startInput() + + select { + case <-t.ctx.Done(): + return nil + case err := <-errCh: + if isNormalExecClose(err, t.isUserInitiatedClose()) { + return nil + } + return err + } +} diff --git a/internal/exec/interactive_terminal_unix.go b/internal/exec/interactive_terminal_unix.go new file mode 100644 index 000000000..aba20d18b --- /dev/null +++ b/internal/exec/interactive_terminal_unix.go @@ -0,0 +1,42 @@ +//go:build !windows +// +build !windows + +package exec + +import ( + "os" + + "golang.org/x/term" +) + +func (t *interactiveTerminal) run() error { + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return err + } + defer func() { + if oldState != nil { + _ = term.Restore(int(os.Stdin.Fd()), oldState) + } + }() + + return t.runPlatform(func() { + go func() { + buf := make([]byte, 1) + for { + select { + case <-t.ctx.Done(): + return + default: + n, readErr := os.Stdin.Read(buf) + if readErr != nil || n == 0 { + continue + } + if t.handleInput(buf[:n]) { + return + } + } + } + }() + }) +} diff --git a/internal/exec/interactive_terminal_windows.go b/internal/exec/interactive_terminal_windows.go new file mode 100644 index 000000000..15145498e --- /dev/null +++ b/internal/exec/interactive_terminal_windows.go @@ -0,0 +1,42 @@ +//go:build windows +// +build windows + +package exec + +import ( + "os" + + "golang.org/x/term" +) + +func (t *interactiveTerminal) run() error { + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return err + } + defer func() { + if oldState != nil { + _ = term.Restore(int(os.Stdin.Fd()), oldState) + } + }() + + return t.runPlatform(func() { + go func() { + buf := make([]byte, 1) + for { + select { + case <-t.ctx.Done(): + return + default: + n, readErr := os.Stdin.Read(buf) + if readErr != nil || n == 0 { + continue + } + if t.handleInput(buf[:n]) { + return + } + } + } + }() + }) +} diff --git a/internal/exec/lookup.go b/internal/exec/lookup.go new file mode 100644 index 000000000..87bca8bd4 --- /dev/null +++ b/internal/exec/lookup.go @@ -0,0 +1,29 @@ +package exec + +import ( + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" +) + +func lookupMicroservice(clt *client.Client, fqName string) (*client.MicroserviceInfo, bool, error) { + appName, msvcName, err := clientutil.ParseFQName(fqName, "Microservice") + if err != nil { + return nil, false, err + } + + msvc, err := clt.GetMicroserviceByName(appName, msvcName) + if err != nil { + if strings.Contains(err.Error(), "Invalid application id") { + msvc, err = clt.GetSystemMicroserviceByName(appName, msvcName) + if err != nil { + return nil, false, err + } + return msvc, true, nil + } + return nil, false, err + } + + return msvc, false, nil +} diff --git a/internal/exec/microservice.go b/internal/exec/microservice.go index f126a4b5e..7dc76caf8 100644 --- a/internal/exec/microservice.go +++ b/internal/exec/microservice.go @@ -1,42 +1,19 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package exec import ( - "fmt" - "net/http" - "strings" - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/internal/util/terminal" - "github.com/eclipse-iofog/iofogctl/internal/util/websocket" - "github.com/eclipse-iofog/iofogctl/pkg/util" ) type microserviceExecutor struct { namespace string name string - client *client.Client - msvc *client.MicroserviceInfo } func newMicroserviceExecutor(namespace, name string) *microserviceExecutor { - a := µserviceExecutor{} - a.namespace = namespace - a.name = name - return a + return µserviceExecutor{ + namespace: namespace, + name: name, + } } func (exe *microserviceExecutor) GetName() string { @@ -44,87 +21,8 @@ func (exe *microserviceExecutor) GetName() string { } func (exe *microserviceExecutor) Execute() error { - util.SpinStart("Connecting Exec Session to Microservice") - - // Init client - clt, err := clientutil.NewControllerClient(exe.namespace) - if err != nil { - return err - } - - appName, msvcName, err := clientutil.ParseFQName(exe.name, "Microservice") - if err != nil { - return err - } - - exe.msvc, err = clt.GetMicroserviceByName(appName, msvcName) - isSystem := false - if err != nil { - // Check if error indicates application not found - if strings.Contains(err.Error(), "Invalid application id") { - // Try system application - exe.msvc, err = clt.GetSystemMicroserviceByName(appName, msvcName) - if err != nil { - return err - } - isSystem = true - } else { - // Return other types of errors - return err - } - } - - // Create WebSocket client - wsClient := websocket.NewClient(exe.msvc.UUID) - - // Get controller endpoint - controllerURL := clt.GetBaseURL() - // Convert http(s):// to ws(s):// - wsURL := strings.Replace(controllerURL, "http://", "ws://", 1) - wsURL = strings.Replace(wsURL, "https://", "wss://", 1) - // Use system endpoint for system microservices - if isSystem { - wsURL = fmt.Sprintf("%s/microservices/system/exec/%s", wsURL, exe.msvc.UUID) - } else { - wsURL = fmt.Sprintf("%s/microservices/exec/%s", wsURL, exe.msvc.UUID) - } - - // Set up headers - headers := http.Header{} - headers.Set("Authorization", fmt.Sprintf("Bearer %s", clt.GetAccessToken())) - util.SpinHandlePrompt() - // Connect to WebSocket - if err := wsClient.Connect(wsURL, headers); err != nil { - util.SpinHandlePromptComplete() - return util.NewError(fmt.Sprintf("failed to connect to WebSocket: %v", err)) - } - - // Create and start terminal - term := terminal.NewTerminal(wsClient) - - // Check for initial connection error - if err := wsClient.GetError(); err != nil { - util.SpinHandlePromptComplete() - formattedErr := formatWebSocketError(err) - return util.NewError(formattedErr) - } - - if err := term.Start(); err != nil { - util.SpinHandlePromptComplete() - formattedErr := formatWebSocketError(err) - return util.NewError(formattedErr) - } - - // Wait for terminal to finish - <-wsClient.GetDone() - msg := fmt.Sprintf("Successfully closed Microservice %s Exec Session", exe.name) - util.PrintSuccess(msg) - - // Check if there was an error - if err := wsClient.GetError(); err != nil { - formattedErr := formatWebSocketError(err) - return util.NewError(formattedErr) - } - - return nil + label := "Microservice " + exe.name + return runExecSession(exe.namespace, label, func(clt *client.Client) (*client.ExecSession, error) { + return dialMicroserviceExec(clt, exe.name) + }) } diff --git a/internal/exec/session.go b/internal/exec/session.go new file mode 100644 index 000000000..a5d2e65e0 --- /dev/null +++ b/internal/exec/session.go @@ -0,0 +1,68 @@ +package exec + +import ( + "fmt" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func runExecSession(namespace, resourceLabel string, dial func(*client.Client) (*client.ExecSession, error)) error { + util.SpinStart(fmt.Sprintf("Connecting Exec Session to %s", resourceLabel)) + + clt, err := clientutil.NewControllerClient(namespace) + if err != nil { + return err + } + + util.SpinHandlePrompt() + session, err := dial(clt) + if err != nil { + util.SpinHandlePromptComplete() + return util.NewError(formatExecError(err)) + } + util.SpinStop() + + term := newInteractiveTerminal(session) + if err := term.run(); err != nil { + return util.NewError(formatExecError(err)) + } + + util.WriteStdout([]byte("\n")) + util.PrintSuccess(fmt.Sprintf("Successfully closed %s Exec Session", resourceLabel)) + return nil +} + +func execDialOptions() *client.DialExecOptions { + return &client.DialExecOptions{ + OnStatusLine: writeExecStatusLine, + } +} + +func writeExecStatusLine(line string) { + data := []byte(line) + if len(data) == 0 || data[len(data)-1] != '\n' { + data = append(data, '\n') + } + util.WriteStdout(data) +} + +func dialExecForMicroservice(clt *client.Client, msvc *client.MicroserviceInfo, isSystem bool) (*client.ExecSession, error) { + opts := execDialOptions() + if isSystem { + return clt.DialSystemMicroserviceExecWithOptions(msvc.UUID, opts) + } + return clt.DialMicroserviceExecWithOptions(msvc.UUID, opts) +} + +func dialMicroserviceExec(clt *client.Client, fqName string) (*client.ExecSession, error) { + msvc, isSystem, err := lookupMicroservice(clt, fqName) + if err != nil { + return nil, err + } + if msvc.Status.Status != "RUNNING" { + return nil, util.NewError(ErrMsgMicroserviceNotRunning) + } + return dialExecForMicroservice(clt, msvc, isSystem) +} diff --git a/internal/exec/shell_exit.go b/internal/exec/shell_exit.go new file mode 100644 index 000000000..fb7e267fb --- /dev/null +++ b/internal/exec/shell_exit.go @@ -0,0 +1,69 @@ +package exec + +import ( + "fmt" + "strconv" + "strings" +) + +func isShellExitInput(data, lineBuffer string) bool { + if strings.ContainsRune(data, '\x04') { + return true + } + if !strings.ContainsAny(data, "\r\n") { + return false + } + line := strings.TrimSpace(strings.NewReplacer("\r", "", "\n", "").Replace(lineBuffer)) + return line == "exit" || line == "logout" +} + +func parseExecWSCloseCode(err error) (int, bool) { + if err == nil { + return 0, false + } + const prefix = "exec WebSocket closed (code " + msg := err.Error() + idx := strings.Index(msg, prefix) + if idx < 0 { + return 0, false + } + rest := msg[idx+len(prefix):] + end := strings.IndexByte(rest, ')') + if end < 0 { + return 0, false + } + code, convErr := strconv.Atoi(rest[:end]) + if convErr != nil { + return 0, false + } + return code, true +} + +func isNormalExecClose(err error, userInitiatedClose bool) bool { + if err == nil { + return true + } + + if code, ok := parseExecWSCloseCode(err); ok { + if code == 1000 { + return true + } + if userInitiatedClose && code == 1005 { + return true + } + } + + errStr := err.Error() + if userInitiatedClose && strings.HasPrefix(errStr, "exec WebSocket closed (code") { + return true + } + if strings.Contains(errStr, "use of closed network connection") { + return true + } + + return false +} + +func execCloseCodeString(code int) string { + return fmt.Sprintf("exec WebSocket closed (code %d)", code) +} diff --git a/internal/exec/shell_exit_test.go b/internal/exec/shell_exit_test.go new file mode 100644 index 000000000..ad13f774d --- /dev/null +++ b/internal/exec/shell_exit_test.go @@ -0,0 +1,44 @@ +package exec + +import ( + "fmt" + "testing" +) + +func TestIsShellExitInput(t *testing.T) { + tests := []struct { + name string + data string + lineBuffer string + want bool + }{ + {"exit command", "exit\r", "exit", true}, + {"logout command", "logout\n", "logout", true}, + {"ctrl+d", "\x04", "", true}, + {"partial exit", "ex", "ex", false}, + {"other command", "ls\r", "ls", false}, + {"exit with spaces", "exit\r", " exit ", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isShellExitInput(tt.data, tt.lineBuffer); got != tt.want { + t.Fatalf("isShellExitInput(%q, %q) = %v, want %v", tt.data, tt.lineBuffer, got, tt.want) + } + }) + } +} + +func TestIsNormalExecClose(t *testing.T) { + err1005 := fmt.Errorf("%s: ", execCloseCodeString(1005)) + err1000 := fmt.Errorf("%s: done", execCloseCodeString(1000)) + + if !isNormalExecClose(err1000, false) { + t.Fatal("expected code 1000 to be normal") + } + if isNormalExecClose(err1005, false) { + t.Fatal("expected code 1005 without user close to be abnormal") + } + if !isNormalExecClose(err1005, true) { + t.Fatal("expected code 1005 with user close to be normal") + } +} diff --git a/internal/exec/utils.go b/internal/exec/utils.go index 4466a2bd7..b8816391e 100644 --- a/internal/exec/utils.go +++ b/internal/exec/utils.go @@ -1,192 +1,100 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package exec import ( + "errors" "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" ) -// Error message constants to avoid duplication const ( - ErrMsgAnotherUserConnected = "Another user is already connected to this microservice. Only one user can connect at a time." - ErrMsgTimeoutWaitingForAgent = "Timeout waiting for agent connection. Please ensure the microservice/agent is running and try again." - ErrMsgAuthenticationFailed = "Authentication failed. Please check your credentials and try again." - ErrMsgMicroserviceNotRunning = "Microservice is not running. Please start the microservice first." - ErrMsgExecNotEnabled = "Microservice exec is not enabled. Please enable exec for this microservice." - ErrMsgNoAvailableExecSession = "No available exec session for this agent or microservice. Be sure to attach/link exec session to the agent or microservice first. If you already attached/linked exec session to the agent or microservice, please wait for the exec session to be ready." - ErrMsgInsufficientPermissions = "Insufficient permissions. Required roles: SRE for Node Exec or Developer for Microservice Exec." - ErrMsgOnlySREAccess = "Only SRE can access system microservices. Please contact your administrator." - ErrMsgConnectionLost = "Connection lost unexpectedly" - ErrMsgMessageTooLarge = "Message too large" - ErrMsgServerError = "Server error occurred" - ErrMsgFailedToConnect = "Failed to connect to server" - ErrMsgConnectionClosed = "Connection was closed" + ErrMsgExecSessionQuotaExceeded = "Maximum of 3 concurrent exec sessions allowed for this microservice." + ErrMsgTimeoutWaitingForAgent = "Timeout waiting for agent connection. Please ensure the microservice/agent is running and try again." + ErrMsgAuthenticationFailed = "Authentication failed. Please check your credentials and try again." + ErrMsgMicroserviceNotRunning = "Microservice is not running. Please start the microservice first." + ErrMsgDebugMicroserviceMissing = "Debug microservice not found. Run attach exec agent first to provision fog debug exec." + ErrMsgDebugContainerTimeout = "Timeout waiting for debug container to start. Please ensure the Agent is running and try again." + ErrMsgAgentNotRunning = "Agent is not running. Start the Agent before opening an exec session." + ErrMsgInsufficientPermissions = "Insufficient permissions. Required roles: SRE for Node Exec or Developer for Microservice Exec." + ErrMsgOnlySREAccess = "Only SRE can access system microservices. Please contact your administrator." + ErrMsgExecRouterUnavailable = "Exec session router unavailable. Retry or check Controller HA configuration." + ErrMsgConnectionLost = "Connection lost unexpectedly" + ErrMsgMessageTooLarge = "Message too large" + ErrMsgServerError = "Server error occurred" + ErrMsgFailedToConnect = "Failed to connect to exec session" + ErrMsgConnectionClosed = "Connection was closed" ) -// formatWebSocketError formats WebSocket errors for better user experience -func formatWebSocketError(err error) string { +func formatExecError(err error) string { if err == nil { return "" } - errStr := err.Error() - - // Handle specific WebSocket error patterns - if strings.Contains(errStr, "close 1008") { - // Extract the reason from the error message - reason := extractCloseReason(errStr) - - // Try direct string matching first (more reliable) - if strings.Contains(errStr, "No available exec session") { - return ErrMsgNoAvailableExecSession - } - if strings.Contains(errStr, "Microservice has already active exec session") { - return ErrMsgAnotherUserConnected - } - if strings.Contains(errStr, "Timeout waiting for agent connection") { - return ErrMsgTimeoutWaitingForAgent - } - if strings.Contains(errStr, "Authentication failed") { - return ErrMsgAuthenticationFailed - } - if strings.Contains(errStr, "Microservice is not running") { - return ErrMsgMicroserviceNotRunning - } - if strings.Contains(errStr, "Microservice exec is not enabled") { - return ErrMsgExecNotEnabled - } - if strings.Contains(errStr, "Microservice already has an active session") { - return ErrMsgAnotherUserConnected - } - if strings.Contains(errStr, "Insufficient permissions") { - return ErrMsgInsufficientPermissions - } - if strings.Contains(errStr, "Only SRE can access system microservices") { - return ErrMsgOnlySREAccess - } + switch { + case errors.Is(err, client.ErrExecSessionQuotaExceeded): + return ErrMsgExecSessionQuotaExceeded + case errors.Is(err, client.ErrExecAgentTimeout): + return ErrMsgTimeoutWaitingForAgent + case errors.Is(err, client.ErrMicroserviceNotRunning): + return ErrMsgMicroserviceNotRunning + case errors.Is(err, client.ErrExecRouterUnavailable): + return ErrMsgExecRouterUnavailable + } - // If no direct match found, try the extracted reason - if reason != "" { - return reason - } + errStr := err.Error() - // Default fallback for unknown 1008 errors - return "Policy violation: Access denied" + if strings.Contains(errStr, "Authentication failed") { + return ErrMsgAuthenticationFailed + } + if strings.Contains(errStr, "Insufficient permissions") { + return ErrMsgInsufficientPermissions + } + if strings.Contains(errStr, "Only SRE can access system microservices") { + return ErrMsgOnlySREAccess + } + if strings.Contains(errStr, "Maximum of 3 concurrent exec sessions") { + return ErrMsgExecSessionQuotaExceeded + } + if strings.Contains(errStr, "Timeout waiting for agent connection") { + return ErrMsgTimeoutWaitingForAgent + } + if strings.Contains(errStr, "not running") || strings.Contains(errStr, "Not running") { + return ErrMsgMicroserviceNotRunning } - if strings.Contains(errStr, "close 1006") { return ErrMsgConnectionLost } - if strings.Contains(errStr, "close 1009") { return ErrMsgMessageTooLarge } - if strings.Contains(errStr, "close 1011") { return ErrMsgServerError } - - if strings.Contains(errStr, "failed to connect") { + if strings.Contains(errStr, "exec WebSocket upgrade failed") || strings.Contains(errStr, "exec WebSocket dial") { return ErrMsgFailedToConnect } - if strings.Contains(errStr, "use of closed network connection") { return ErrMsgConnectionClosed } + if strings.Contains(errStr, execCloseCodeString(1000)) { + return "" + } - // Default case - return the original error but clean it up - if strings.Contains(errStr, "websocket: close") { - // Extract the reason part if available - if idx := strings.Index(errStr, "reason:"); idx != -1 { - reason := strings.TrimSpace(errStr[idx+7:]) - return reason - } - // Extract the code and basic message - if strings.Contains(errStr, "failed to read message:") { - parts := strings.Split(errStr, "failed to read message:") - if len(parts) > 1 { - return strings.TrimSpace(parts[1]) - } - } + if reason := extractCloseReason(errStr); reason != "" { + return reason } return errStr } -// extractCloseReason extracts the reason from WebSocket close error messages func extractCloseReason(errStr string) string { - // Look for "reason:" pattern if idx := strings.Index(errStr, "reason:"); idx != -1 { - reason := strings.TrimSpace(errStr[idx+7:]) - // Remove trailing period if present - if strings.HasSuffix(reason, ".") { - reason = reason[:len(reason)-1] - } - return reason + return strings.TrimSuffix(strings.TrimSpace(errStr[idx+7:]), ".") } - - // Look for "policy violation:" pattern - if idx := strings.Index(errStr, "policy violation:"); idx != -1 { - reason := strings.TrimSpace(errStr[idx+18:]) - // Remove trailing period if present - if strings.HasSuffix(reason, ".") { - reason = reason[:len(reason)-1] + if idx := strings.Index(errStr, "exec WebSocket closed (code"); idx != -1 { + if colon := strings.LastIndex(errStr, ": "); colon != -1 { + return strings.TrimSpace(errStr[colon+2:]) } - return reason } - - // Look for quoted reason at the end - if strings.Contains(errStr, "close 1008") { - // Try to extract the last quoted string - parts := strings.Split(errStr, `"`) - if len(parts) >= 2 { - lastPart := parts[len(parts)-2] // Get the second-to-last part (the quoted reason) - if lastPart != "" { - return lastPart - } - } - - // Try to extract after "close 1008" - if idx := strings.Index(errStr, "close 1008"); idx != -1 { - afterClose := strings.TrimSpace(errStr[idx+10:]) - // Remove parentheses and other formatting - afterClose = strings.TrimPrefix(afterClose, "(") - afterClose = strings.TrimSuffix(afterClose, ")") - afterClose = strings.TrimSpace(afterClose) - - // If it starts with a quote, extract the quoted part - if strings.HasPrefix(afterClose, `"`) { - if endIdx := strings.Index(afterClose[1:], `"`); endIdx != -1 { - return afterClose[1 : endIdx+1] - } - } - - // If it contains a colon, extract after the colon - if colonIdx := strings.Index(afterClose, ":"); colonIdx != -1 { - reason := strings.TrimSpace(afterClose[colonIdx+1:]) - if strings.HasSuffix(reason, ".") { - reason = reason[:len(reason)-1] - } - return reason - } - - // Return the whole thing if it looks like a reason - if len(afterClose) > 0 && !strings.Contains(afterClose, "websocket") { - return afterClose - } - } - } - return "" } diff --git a/internal/execute/executor.go b/internal/execute/executor.go index 1306292cf..2ff1eda62 100644 --- a/internal/execute/executor.go +++ b/internal/execute/executor.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package execute type Executor interface { diff --git a/internal/execute/parallel.go b/internal/execute/parallel.go index 8b97056b4..73e5bfa87 100644 --- a/internal/execute/parallel.go +++ b/internal/execute/parallel.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package execute import ( diff --git a/internal/execute/utils.go b/internal/execute/utils.go index 59507cd98..27b8c0728 100644 --- a/internal/execute/utils.go +++ b/internal/execute/utils.go @@ -1,23 +1,10 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package execute import ( "bytes" + "errors" "fmt" "io" - "os" "github.com/eclipse-iofog/iofogctl/internal/config" "github.com/eclipse-iofog/iofogctl/pkg/util" @@ -56,7 +43,7 @@ func NewEmptyExecutor(name string) Executor { } } -func generateExecutor(header *config.Header, namespace string, kindHandlers map[config.Kind]func(*KindHandlerOpt) (Executor, error)) (exe Executor, err error) { +func generateExecutor(header *config.Header, namespace string, deleteNamespace bool, kindHandlers map[config.Kind]func(*KindHandlerOpt) (Executor, error)) (exe Executor, err error) { if len(header.Metadata.Namespace) > 0 && namespace != header.Metadata.Namespace { msg := "The Namespace provided by the %s named '%s' does not match the Namespace '%s'. You must pass '--namespace %s' to perform this command" return nil, util.NewInputError(fmt.Sprintf(msg, header.Kind, header.Metadata.Name, namespace, header.Metadata.Namespace)) @@ -91,28 +78,30 @@ func generateExecutor(header *config.Header, namespace string, kindHandlers map[ } return createExecutorFunc(&KindHandlerOpt{ - Kind: header.Kind, - Namespace: namespace, - Name: header.Metadata.Name, - YAML: subYamlBytes, - FullYAML: fullYamlBytes, - Data: dataYamlBytes, - Tags: header.Metadata.Tags, + Kind: header.Kind, + Namespace: namespace, + Name: header.Metadata.Name, + YAML: subYamlBytes, + FullYAML: fullYamlBytes, + Data: dataYamlBytes, + Tags: header.Metadata.Tags, + DeleteNamespace: deleteNamespace, }) } type KindHandlerOpt struct { - Kind config.Kind - Namespace string - Name string - YAML []byte - FullYAML []byte - Data []byte - Tags *[]string + Kind config.Kind + Namespace string + Name string + YAML []byte + FullYAML []byte + Data []byte + Tags *[]string + DeleteNamespace bool } -func GetExecutorsFromYAML(inputFile, namespace string, kindHandlers map[config.Kind]func(*KindHandlerOpt) (Executor, error)) (executorsMap map[config.Kind][]Executor, err error) { - yamlFile, err := os.ReadFile(inputFile) +func GetExecutorsFromYAML(inputFile, namespace string, kindHandlers map[config.Kind]func(*KindHandlerOpt) (Executor, error), deleteNamespace bool) (executorsMap map[config.Kind][]Executor, err error) { + yamlFile, err := util.ReadUserFile(inputFile) if err != nil { return } @@ -129,7 +118,7 @@ func GetExecutorsFromYAML(inputFile, namespace string, kindHandlers map[config.K decodeErr := dec.Decode(&h) for decodeErr == nil { header := headerDecodeToHeader(&h) - exe, err := generateExecutor(header, namespace, kindHandlers) + exe, err := generateExecutor(header, namespace, deleteNamespace, kindHandlers) if err != nil { return nil, err } @@ -143,7 +132,7 @@ func GetExecutorsFromYAML(inputFile, namespace string, kindHandlers map[config.K decodeErr = dec.Decode(&h) } - if decodeErr != io.EOF { + if !errors.Is(decodeErr, io.EOF) { return nil, decodeErr } @@ -157,16 +146,21 @@ func GetExecutorsFromYAML(inputFile, namespace string, kindHandlers map[config.K // headerDecodeToHeader converts headerDecode to config.Header, building Spec from // top-level rules/roleRef/subjects when present (Controller-style RBAC YAML). func headerDecodeToHeader(h *headerDecode) *config.Header { + kind := h.Kind + if kind == "RemoteController" { + kind = config.RemoteControllerKind + } + header := &config.Header{ APIVersion: h.APIVersion, - Kind: h.Kind, + Kind: kind, Metadata: h.Metadata, Spec: h.Spec, Data: h.Data, Status: h.Status, } - switch h.Kind { + switch kind { case config.RoleKind: if h.Rules != nil { // Controller-style: rules at top level diff --git a/internal/execute/utils_test.go b/internal/execute/utils_test.go new file mode 100644 index 000000000..009a6e590 --- /dev/null +++ b/internal/execute/utils_test.go @@ -0,0 +1,18 @@ +package execute + +import ( + "testing" + + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/stretchr/testify/require" +) + +func TestHeaderDecodeToHeaderRemoteControllerAlias(t *testing.T) { + header := headerDecodeToHeader(&headerDecode{Kind: "RemoteController"}) + require.Equal(t, config.RemoteControllerKind, header.Kind) +} + +func TestHeaderDecodeToHeaderControllerUnchanged(t *testing.T) { + header := headerDecodeToHeader(&headerDecode{Kind: config.RemoteControllerKind}) + require.Equal(t, config.RemoteControllerKind, header.Kind) +} diff --git a/internal/get/agents.go b/internal/get/agents.go index fd7352cd0..73446a285 100644 --- a/internal/get/agents.go +++ b/internal/get/agents.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/all.go b/internal/get/all.go index 9dae5811a..2749d6c25 100644 --- a/internal/get/all.go +++ b/internal/get/all.go @@ -1,21 +1,7 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( "github.com/eclipse-iofog/iofogctl/internal/config" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" ) type tableFunc = func(string, tableChannel) @@ -59,11 +45,6 @@ func (exe *allExecutor) Execute() error { return err } - // Add edge resource output if supported - if err := clientutil.IsEdgeResourceCapable(exe.namespace); err == nil { - // Add Edge Resources between Agent and Application - routines = append(routines[:2], append([]tableFunc{getEdgeResourceTable}, routines[2:]...)...) - } // Get tables in parallel tableChans := make([]tableChannel, len(routines)) for idx := range tableChans { @@ -136,14 +117,6 @@ func getVolumeTable(namespace string, tableChan tableChannel) { } } -func getEdgeResourceTable(namespace string, tableChan tableChannel) { - table, err := generateEdgeResourceOutput(namespace) - tableChan <- tableQuery{ - table: table, - err: err, - } -} - func getServiceTable(namespace string, tableChan tableChannel) { table, err := generateServicesOutput(namespace) if err != nil { diff --git a/internal/get/applications.go b/internal/get/applications.go index c5599c9b2..f67c4122a 100644 --- a/internal/get/applications.go +++ b/internal/get/applications.go @@ -1,19 +1,7 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( + "errors" "fmt" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" @@ -25,7 +13,7 @@ import ( type applicationExecutor struct { namespace string client *client.Client - flows []client.FlowInfo + applications []client.ApplicationInfo msvcsPerApplication map[int][]*client.MicroserviceInfo natsPerApplication map[int]*client.ApplicationNatsConfig } @@ -62,7 +50,8 @@ func (exe *applicationExecutor) init() (err error) { } applications, err := exe.client.GetAllApplications() // Try legacy if error is "not found" - if _, ok := err.(*client.NotFoundError); ok { + notFoundError := &client.NotFoundError{} + if errors.As(err, ¬FoundError) { if err := exe.initLegacy(); err != nil { return err } @@ -74,11 +63,11 @@ func (exe *applicationExecutor) init() (err error) { return err } // Execute non-legacy - // Map applications to flow - // TODO: Use Application instead of flow - exe.flows = []client.FlowInfo{} + // Map applications to application + // TODO: Use Application instead of application + exe.applications = []client.ApplicationInfo{} for _, application := range applications.Applications { - exe.flows = append(exe.flows, client.FlowInfo{ + exe.applications = append(exe.applications, client.ApplicationInfo{ Name: application.Name, IsActivated: application.IsActivated, Description: application.Description, @@ -106,18 +95,18 @@ func (exe *applicationExecutor) init() (err error) { func (exe *applicationExecutor) generateApplicationOutput() (table [][]string) { // Generate table and headers - table = make([][]string, len(exe.flows)+1) + table = make([][]string, len(exe.applications)+1) headers := []string{"APPLICATION", "RUNNING", "NATS ACCESS", "MICROSERVICES"} table[0] = append(table[0], headers...) // Populate rows - for idx, flow := range exe.flows { - nbMsvcs := len(exe.msvcsPerApplication[flow.ID]) + for idx, application := range exe.applications { + nbMsvcs := len(exe.msvcsPerApplication[application.ID]) runningMsvcs := 0 msvcs := "" first := true - for idx := range exe.msvcsPerApplication[flow.ID] { - msvc := exe.msvcsPerApplication[flow.ID][idx] + for idx := range exe.msvcsPerApplication[application.ID] { + msvc := exe.msvcsPerApplication[application.ID][idx] if first { msvcs += msvc.Name } else { @@ -130,17 +119,17 @@ func (exe *applicationExecutor) generateApplicationOutput() (table [][]string) { } if nbMsvcs > 5 { - msvcs = fmt.Sprintf("%d microservices", len(exe.msvcsPerApplication[flow.ID])) + msvcs = fmt.Sprintf("%d microservices", len(exe.msvcsPerApplication[application.ID])) } status := fmt.Sprintf("%d/%d", runningMsvcs, nbMsvcs) natsAccess := "false" - if natsConfig := exe.natsPerApplication[flow.ID]; natsConfig != nil && natsConfig.NatsAccess { + if natsConfig := exe.natsPerApplication[application.ID]; natsConfig != nil && natsConfig.NatsAccess { natsAccess = "true" } row := []string{ - flow.Name, + application.Name, status, natsAccess, msvcs, diff --git a/internal/get/applications_legacy.go b/internal/get/applications_legacy.go index 8b00bf59a..314d38881 100644 --- a/internal/get/applications_legacy.go +++ b/internal/get/applications_legacy.go @@ -1,28 +1,15 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import "github.com/eclipse-iofog/iofogctl/pkg/util" func (exe *applicationExecutor) initLegacy() (err error) { - flows, err := exe.client.GetAllFlows() + applications, err := exe.client.GetAllApplications() if err != nil { return } - exe.flows = flows.Flows - for _, flow := range exe.flows { - listMsvcs, err := exe.client.GetMicroservicesPerFlow(flow.ID) + exe.applications = applications.Applications + for _, application := range exe.applications { + listMsvcs, err := exe.client.GetMicroservicesByApplication(application.Name) if err != nil { return err } @@ -33,7 +20,7 @@ func (exe *applicationExecutor) initLegacy() (err error) { if util.IsSystemMsvc(msvc) { continue } - exe.msvcsPerApplication[flow.ID] = append(exe.msvcsPerApplication[flow.ID], msvc) + exe.msvcsPerApplication[application.ID] = append(exe.msvcsPerApplication[application.ID], msvc) } } return nil diff --git a/internal/get/catalog.go b/internal/get/catalog.go index 5ad59abb9..00e879ee8 100644 --- a/internal/get/catalog.go +++ b/internal/get/catalog.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( @@ -67,9 +54,13 @@ func generateCatalogOutput(namespace string) error { Registry: client.RegistryTypeIDRegistryTypeDict[item.RegistryID], } for _, image := range item.Images { - switch client.AgentTypeIDAgentTypeDict[image.AgentTypeID] { - case "x86": - catalogItem.X86 = image.ContainerImage + switch client.ArchIDToName[image.ArchID] { + case "amd64": + catalogItem.AMD64 = image.ContainerImage + case "arm64": + catalogItem.ARM64 = image.ContainerImage + case "riscv64": + catalogItem.RISCV64 = image.ContainerImage case "arm": catalogItem.ARM = image.ContainerImage default: @@ -89,7 +80,9 @@ func tabulateCatalogItems(catalogItems []apps.CatalogItem) error { "NAME", "DESCRIPTION", "REGISTRY", - "X86", + "AMD64", + "ARM64", + "RISCV64", "ARM", } table[0] = append(table[0], headers...) @@ -101,7 +94,9 @@ func tabulateCatalogItems(catalogItems []apps.CatalogItem) error { item.Name, item.Description, item.Registry, - item.X86, + item.AMD64, + item.ARM64, + item.RISCV64, item.ARM, } table[idx+1] = append(table[idx+1], row...) diff --git a/internal/get/certificates.go b/internal/get/certificates.go index dfca1d548..1090a1ad9 100644 --- a/internal/get/certificates.go +++ b/internal/get/certificates.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/config_maps.go b/internal/get/config_maps.go index bee7653d7..6b51fa31a 100644 --- a/internal/get/config_maps.go +++ b/internal/get/config_maps.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/controllers.go b/internal/get/controllers.go index c506f36dd..becf1df98 100644 --- a/internal/get/controllers.go +++ b/internal/get/controllers.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( @@ -150,11 +137,6 @@ func updateControllerPods(controlPlane *rsc.KubernetesControlPlane, namespace st installer.SetHttpsEnabled(controlPlane.Controller.Https) } - if controlPlane.Controller.EcnViewerURL != "" { - viewerDns := true - installer.SetIsViewerDns(&viewerDns) - } - pods, err := installer.GetControllerPods() if err != nil { return diff --git a/internal/get/edge_resources.go b/internal/get/edge_resources.go deleted file mode 100644 index f85d24bef..000000000 --- a/internal/get/edge_resources.go +++ /dev/null @@ -1,107 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package get - -import ( - "fmt" - - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/internal/config" - rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" -) - -type edgeResourceExecutor struct { - namespace string -} - -func newEdgeResourceExecutor(namespace string) *edgeResourceExecutor { - return &edgeResourceExecutor{ - namespace: namespace, - } -} - -func (exe *edgeResourceExecutor) GetName() string { - return "" -} - -func (exe *edgeResourceExecutor) Execute() error { - printNamespace(exe.namespace) - table, err := generateEdgeResourceOutput(exe.namespace) - if err != nil { - return err - } - return print(table) -} - -func generateEdgeResourceOutput(namespace string) (table [][]string, err error) { - _, err = config.GetNamespace(namespace) - if err != nil { - return - } - - // Connect to Controller - clt, err := clientutil.NewControllerClient(namespace) - if err != nil && !rsc.IsNoControlPlaneError(err) { - return - } - - edgeResources := []client.EdgeResourceMetadata{} - if err == nil { - // Populate table - listResponse, err := clt.ListEdgeResources() - if err != nil { - return table, err - } - edgeResources = listResponse.EdgeResources - } - - return tabulateEdgeResources(edgeResources) -} - -func tabulateEdgeResources(edgeResources []client.EdgeResourceMetadata) (table [][]string, err error) { - // Generate table and headers - table = make([][]string, len(edgeResources)+1) - headers := []string{"EDGE RESOURCE", "PROTOCOL", "VERSIONS"} - table[0] = append(table[0], headers...) - - // Coalesce versions - index := make(map[string]client.EdgeResourceMetadata) - for i := range edgeResources { - edgeResource := edgeResources[i] - name := edgeResource.Name - if indexEdgeResource, exists := index[name]; exists { - // Append version - indexEdgeResource.Version = fmt.Sprintf("%s, %s", index[name].Version, edgeResource.Version) - index[name] = indexEdgeResource - } else { - // Instantiate new resource - index[name] = edgeResource - } - } - // Populate rows - idx := 0 - for i := range index { - edge := index[i] - // Store values - row := []string{ - edge.Name, - edge.InterfaceProtocol, - edge.Version, - } - table[idx+1] = append(table[idx+1], row...) - idx++ - } - return table, err -} diff --git a/internal/get/factory.go b/internal/get/factory.go index 8835f6019..ac8625d7e 100644 --- a/internal/get/factory.go +++ b/internal/get/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( @@ -44,8 +31,6 @@ func NewExecutor(resourceType, namespace string, showDetached bool) (execute.Exe return newRegistryExecutor(namespace), nil case "volumes": return newVolumeExecutor(namespace), nil - case "edge-resources": - return newEdgeResourceExecutor(namespace), nil case "secrets": return newSecretExecutor(namespace), nil case "configmaps": diff --git a/internal/get/microservices.go b/internal/get/microservices.go index 50dde572f..6f4103ace 100644 --- a/internal/get/microservices.go +++ b/internal/get/microservices.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/namespaces.go b/internal/get/namespaces.go index 16ec16031..ac32a64f4 100644 --- a/internal/get/namespaces.go +++ b/internal/get/namespaces.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/print.go b/internal/get/print.go index e1dbacc46..040578882 100644 --- a/internal/get/print.go +++ b/internal/get/print.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/registry.go b/internal/get/registry.go index e6bbd35a2..f236c7676 100644 --- a/internal/get/registry.go +++ b/internal/get/registry.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/role.go b/internal/get/role.go index 79c2c29f3..ac5a90c58 100644 --- a/internal/get/role.go +++ b/internal/get/role.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/rolebinding.go b/internal/get/rolebinding.go index d3f2317a0..5a49bec87 100644 --- a/internal/get/rolebinding.go +++ b/internal/get/rolebinding.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/secrets.go b/internal/get/secrets.go index a4ff2f767..a85358971 100644 --- a/internal/get/secrets.go +++ b/internal/get/secrets.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/serviceaccount.go b/internal/get/serviceaccount.go index 433de705b..8ad5f1304 100644 --- a/internal/get/serviceaccount.go +++ b/internal/get/serviceaccount.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/services.go b/internal/get/services.go index 46f944487..647a5c0cb 100644 --- a/internal/get/services.go +++ b/internal/get/services.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( @@ -99,7 +86,7 @@ func generateServicesOutput(namespace string) ([][]string, error) { strconv.Itoa(service.BridgePort), // service.DefaultBridge, // serviceEndpoint, - service.ProvisioningStatus, + string(service.ProvisioningStatus), } table[idx+1] = append(table[idx+1], row...) } diff --git a/internal/get/system_applications.go b/internal/get/system_applications.go index ff13fa01d..23e6e3fbd 100644 --- a/internal/get/system_applications.go +++ b/internal/get/system_applications.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( @@ -24,7 +11,7 @@ import ( type systemApplicationExecutor struct { namespace string client *client.Client - flows []client.FlowInfo + applications []client.ApplicationInfo msvcsPerApplication map[int][]*client.MicroserviceInfo } @@ -71,11 +58,11 @@ func (exe *systemApplicationExecutor) init() (err error) { return err } // Execute non-legacy - // Map applications to flow - // TODO: Use Application instead of flow - exe.flows = []client.FlowInfo{} + // Map applications to application + // TODO: Use Application instead of application + exe.applications = []client.ApplicationInfo{} for _, application := range applications.Applications { - exe.flows = append(exe.flows, client.FlowInfo{ + exe.applications = append(exe.applications, client.ApplicationInfo{ Name: application.Name, IsActivated: application.IsActivated, Description: application.Description, @@ -102,18 +89,18 @@ func (exe *systemApplicationExecutor) init() (err error) { func (exe *systemApplicationExecutor) generateSystemApplicationOutput() (table [][]string) { // Generate table and headers - table = make([][]string, len(exe.flows)+1) + table = make([][]string, len(exe.applications)+1) headers := []string{"SYS-APPLICATION", "RUNNING", "SYS-MICROSERVICES"} table[0] = append(table[0], headers...) // Populate rows - for idx, flow := range exe.flows { - nbMsvcs := len(exe.msvcsPerApplication[flow.ID]) + for idx, application := range exe.applications { + nbMsvcs := len(exe.msvcsPerApplication[application.ID]) runningMsvcs := 0 msvcs := "" first := true - for idx := range exe.msvcsPerApplication[flow.ID] { - msvc := exe.msvcsPerApplication[flow.ID][idx] + for idx := range exe.msvcsPerApplication[application.ID] { + msvc := exe.msvcsPerApplication[application.ID][idx] if first { msvcs += msvc.Name } else { @@ -126,13 +113,13 @@ func (exe *systemApplicationExecutor) generateSystemApplicationOutput() (table [ } if nbMsvcs > 5 { - msvcs = fmt.Sprintf("%d microservices", len(exe.msvcsPerApplication[flow.ID])) + msvcs = fmt.Sprintf("%d microservices", len(exe.msvcsPerApplication[application.ID])) } status := fmt.Sprintf("%d/%d", runningMsvcs, nbMsvcs) row := []string{ - flow.Name, + application.Name, status, msvcs, } diff --git a/internal/get/system_microservices.go b/internal/get/system_microservices.go index d33597563..2ae0ba42c 100644 --- a/internal/get/system_microservices.go +++ b/internal/get/system_microservices.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/templates.go b/internal/get/templates.go index 163b4ff1c..a74c29cd6 100644 --- a/internal/get/templates.go +++ b/internal/get/templates.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/util.go b/internal/get/util.go index ebe65f3dd..bdad3d826 100644 --- a/internal/get/util.go +++ b/internal/get/util.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/util_test.go b/internal/get/util_test.go index 9c2a5c7f2..85573f748 100644 --- a/internal/get/util_test.go +++ b/internal/get/util_test.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/volume_mounts.go b/internal/get/volume_mounts.go index 8abc8a805..265774b71 100644 --- a/internal/get/volume_mounts.go +++ b/internal/get/volume_mounts.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/get/volumes.go b/internal/get/volumes.go index 80543f254..cc5a20b64 100644 --- a/internal/get/volumes.go +++ b/internal/get/volumes.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package get import ( diff --git a/internal/logs/agent.go b/internal/logs/agent.go index 180c2339f..fd6953afd 100644 --- a/internal/logs/agent.go +++ b/internal/logs/agent.go @@ -1,27 +1,12 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package logs import ( "fmt" - "net/http" - "strings" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/config" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - ws "github.com/eclipse-iofog/iofogctl/internal/util/websocket" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -60,14 +45,14 @@ func (exe *agentExecutor) Execute() error { return err } - switch baseAgent.(type) { + switch agent := baseAgent.(type) { case *rsc.LocalAgent: - lc, err := install.NewLocalContainerClient() + sdkCfg := localAgentSDKConfig(agent) + lc, err := install.NewLocalContainerClient(install.LocalContainerEngineForHostOps(sdkCfg), sdkCfg) if err != nil { return err } - containerName := install.GetLocalContainerName("agent", false) - stdout, stderr, err := lc.GetLogsByName(containerName) + stdout, stderr, err := lc.GetLogsByName(install.EdgeletContainerName) if err != nil { return err } @@ -76,71 +61,32 @@ func (exe *agentExecutor) Execute() error { return nil case *rsc.RemoteAgent: - // Use WebSocket to stream logs from Controller util.SpinStart("Connecting to Agent Logs") - // Init controller client clt, err := clientutil.NewControllerClient(exe.namespace) if err != nil { util.SpinHandlePromptComplete() return err } - // Get agent UUID from controller agentInfo, err := clt.GetAgentByName(exe.name) if err != nil { util.SpinHandlePromptComplete() return fmt.Errorf("failed to get Agent by name: %s", err.Error()) } - // Create WebSocket client (using agent UUID as identifier) - wsClient := ws.NewClient(agentInfo.UUID) - - // Get controller endpoint - controllerURL := clt.GetBaseURL() - // Convert http(s):// to ws(s):// - wsURL := strings.Replace(controllerURL, "http://", "ws://", 1) - wsURL = strings.Replace(wsURL, "https://", "wss://", 1) - wsURL = fmt.Sprintf("%s/iofog/%s/logs", wsURL, agentInfo.UUID) - - // Append query parameters from log config - if exe.logConfig != nil { - queryString := exe.logConfig.BuildQueryString() - if queryString != "" { - wsURL = fmt.Sprintf("%s?%s", wsURL, queryString) - } - } - - // Set up headers - headers := http.Header{} - headers.Set("Authorization", fmt.Sprintf("Bearer %s", clt.GetAccessToken())) - util.SpinHandlePrompt() - // Connect to WebSocket - if err := wsClient.Connect(wsURL, headers); err != nil { - util.SpinHandlePromptComplete() - return util.NewError(fmt.Sprintf("failed to connect to WebSocket: %v", err)) - } - - // Create and start log stream - logStream := NewLogStream(wsClient) - - // Check for initial connection error - if err := wsClient.GetError(); err != nil { - util.SpinHandlePromptComplete() - formattedErr := formatWebSocketError(err) - return util.NewError(formattedErr) - } - - if err := logStream.Start(); err != nil { - util.SpinHandlePromptComplete() - formattedErr := formatWebSocketError(err) - return util.NewError(formattedErr) - } - - // Wait for stream to finish - <-wsClient.GetDone() - util.SpinHandlePromptComplete() + opts := exe.logConfig.ToSDKOptions() + return runRemoteLogStream(clt, func(clt *client.Client) (*client.LogSession, error) { + return clt.DialFogLogs(agentInfo.UUID, opts) + }) } return nil } + +func localAgentSDKConfig(agent *rsc.LocalAgent) *client.AgentConfiguration { + if agent == nil || agent.Config == nil { + return nil + } + return &agent.Config.AgentConfiguration +} diff --git a/internal/logs/config.go b/internal/logs/config.go index 3969da1e9..d571ff65f 100644 --- a/internal/logs/config.go +++ b/internal/logs/config.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package logs import ( @@ -18,12 +5,13 @@ import ( "net/url" "time" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/pkg/util" ) // LogTailConfig holds configuration for log tailing type LogTailConfig struct { - Tail int // Number of lines to tail (default: 100, range: 1-10000) + Tail int // Number of lines to tail (default: 100, range: 1-5000) Follow bool // Whether to follow logs (default: true) Since string // Start time in ISO 8601 format (optional) Until string // End time in ISO 8601 format (optional) @@ -40,8 +28,8 @@ func DefaultLogTailConfig() *LogTailConfig { // Validate validates the LogTailConfig func (c *LogTailConfig) Validate() error { // Validate tail range - if c.Tail < 1 || c.Tail > 10000 { - return util.NewInputError(fmt.Sprintf("tail must be between 1 and 10000, got %d", c.Tail)) + if c.Tail < 1 || c.Tail > 5000 { + return util.NewInputError(fmt.Sprintf("tail must be between 1 and 5000, got %d", c.Tail)) } // Validate ISO 8601 format for since if provided @@ -84,6 +72,19 @@ func (c *LogTailConfig) BuildQueryString() string { return values.Encode() } +// ToSDKOptions converts the CLI log tail config to SDK dial options. +func (c *LogTailConfig) ToSDKOptions() *client.LogTailOptions { + if c == nil { + return nil + } + return &client.LogTailOptions{ + Tail: c.Tail, + Follow: c.Follow, + Since: c.Since, + Until: c.Until, + } +} + // validateISO8601 validates that a string is in ISO 8601 format func validateISO8601(dateStr string) error { // Try parsing with RFC3339 format (ISO 8601 compatible) @@ -92,7 +93,7 @@ func validateISO8601(dateStr string) error { // Try parsing with RFC3339Nano format _, err = time.Parse(time.RFC3339Nano, dateStr) if err != nil { - return fmt.Errorf("invalid ISO 8601 format: %v", err) + return fmt.Errorf("invalid ISO 8601 format: %w", err) } } return nil diff --git a/internal/logs/factory.go b/internal/logs/factory.go index da82e4302..c58b8a5db 100644 --- a/internal/logs/factory.go +++ b/internal/logs/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package logs import ( diff --git a/internal/logs/k8s_controller.go b/internal/logs/k8s_controller.go index 9c75d807a..cab1a476c 100644 --- a/internal/logs/k8s_controller.go +++ b/internal/logs/k8s_controller.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package logs import ( diff --git a/internal/logs/local_controller.go b/internal/logs/local_controller.go index 93ceeeada..4757c1cca 100644 --- a/internal/logs/local_controller.go +++ b/internal/logs/local_controller.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package logs import ( @@ -37,17 +24,15 @@ func (exe *localControllerExecutor) GetName() string { } func (exe *localControllerExecutor) Execute() error { - lc, err := install.NewLocalContainerClient() + lc, err := install.NewLocalContainerClient(install.DefaultLocalContainerEngine, nil) if err != nil { return err } - containerName := install.GetLocalContainerName("controller", false) - stdout, stderr, err := lc.GetLogsByName(containerName) + stdout, stderr, err := lc.GetLogsByName(install.EdgeletContainerName) if err != nil { return err } printContainerLogs(stdout, stderr) - return nil } diff --git a/internal/logs/microservice.go b/internal/logs/microservice.go index 6a79241bf..e84948fe0 100644 --- a/internal/logs/microservice.go +++ b/internal/logs/microservice.go @@ -1,28 +1,12 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package logs import ( - "fmt" - "net/http" "strings" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/config" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - ws "github.com/eclipse-iofog/iofogctl/internal/util/websocket" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -56,9 +40,10 @@ func (ms *remoteMicroserviceExecutor) Execute() error { return util.NewError("The microservice is not currently running") } - switch baseAgent.(type) { + switch agent := baseAgent.(type) { case *rsc.LocalAgent: - lc, err := install.NewLocalContainerClient() + sdkCfg := localAgentSDKConfig(agent) + lc, err := install.NewLocalContainerClient(install.LocalContainerEngineForHostOps(sdkCfg), sdkCfg) if err != nil { return err } @@ -72,67 +57,21 @@ func (ms *remoteMicroserviceExecutor) Execute() error { return nil case *rsc.RemoteAgent: - // Use WebSocket to stream logs from Controller util.SpinStart("Connecting to Microservice Logs") - // Init controller client clt, err := clientutil.NewControllerClient(ms.namespace) if err != nil { util.SpinHandlePromptComplete() return err } - // Create WebSocket client (using microservice UUID) - wsClient := ws.NewClient(msvc.UUID) - - // Get controller endpoint - controllerURL := clt.GetBaseURL() - // Convert http(s):// to ws(s):// - wsURL := strings.Replace(controllerURL, "http://", "ws://", 1) - wsURL = strings.Replace(wsURL, "https://", "wss://", 1) - if isSystem { - wsURL = fmt.Sprintf("%s/microservices/system/%s/logs", wsURL, msvc.UUID) - } else { - wsURL = fmt.Sprintf("%s/microservices/%s/logs", wsURL, msvc.UUID) - } - - // Append query parameters from log config - if ms.logConfig != nil { - queryString := ms.logConfig.BuildQueryString() - if queryString != "" { - wsURL = fmt.Sprintf("%s?%s", wsURL, queryString) + opts := ms.logConfig.ToSDKOptions() + return runRemoteLogStream(clt, func(clt *client.Client) (*client.LogSession, error) { + if isSystem { + return clt.DialSystemMicroserviceLogs(msvc.UUID, opts) } - } - - // Set up headers - headers := http.Header{} - headers.Set("Authorization", fmt.Sprintf("Bearer %s", clt.GetAccessToken())) - util.SpinHandlePrompt() - // Connect to WebSocket - if err := wsClient.Connect(wsURL, headers); err != nil { - util.SpinHandlePromptComplete() - return util.NewError(fmt.Sprintf("failed to connect to WebSocket: %v", err)) - } - - // Create and start log stream - logStream := NewLogStream(wsClient) - - // Check for initial connection error - if err := wsClient.GetError(); err != nil { - util.SpinHandlePromptComplete() - formattedErr := formatWebSocketError(err) - return util.NewError(formattedErr) - } - - if err := logStream.Start(); err != nil { - util.SpinHandlePromptComplete() - formattedErr := formatWebSocketError(err) - return util.NewError(formattedErr) - } - - // Wait for stream to finish - <-wsClient.GetDone() - util.SpinHandlePromptComplete() + return clt.DialMicroserviceLogs(msvc.UUID, opts) + }) } return nil diff --git a/internal/logs/remote_controller.go b/internal/logs/remote_controller.go index eb7d1ee68..07507bdfa 100644 --- a/internal/logs/remote_controller.go +++ b/internal/logs/remote_controller.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package logs import ( @@ -73,7 +60,7 @@ func (exe *remoteControllerExecutor) Execute() error { } // Get logs - out, err := ssh.Run("sudo docker logs iofog-controller") + out, err := ssh.Run("sudo docker logs edgelet 2>&1 || sudo podman logs edgelet 2>&1") if err != nil { return err } diff --git a/internal/logs/stream.go b/internal/logs/stream.go index e92928d7e..1d1fc06a2 100644 --- a/internal/logs/stream.go +++ b/internal/logs/stream.go @@ -1,155 +1,99 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package logs import ( - "context" + "errors" "fmt" - "os" "strings" - "sync" - ws "github.com/eclipse-iofog/iofogctl/internal/util/websocket" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" ) -// LogStream handles streaming logs from WebSocket connection -type LogStream struct { - wsClient *ws.Client - ctx context.Context - cancel context.CancelFunc - cleanupOnce sync.Once - stdoutMutex sync.Mutex -} +func streamLogSession(session *client.LogSession) error { + defer session.Close() -// NewLogStream creates a new LogStream handler -func NewLogStream(wsClient *ws.Client) *LogStream { - ctx, cancel := context.WithCancel(context.Background()) - return &LogStream{ - wsClient: wsClient, - ctx: ctx, - cancel: cancel, - } -} - -func (ls *LogStream) writeToStdout(data []byte) { - ls.stdoutMutex.Lock() - defer ls.stdoutMutex.Unlock() - os.Stdout.Write(data) - os.Stdout.Sync() -} + for { + frame, err := session.Read() + if err != nil { + return util.NewError(formatLogError(err)) + } + if frame == nil { + return nil + } -func (ls *LogStream) cleanup() { - ls.cleanupOnce.Do(func() { - if ls.wsClient != nil { - ls.wsClient.Close() + switch frame.Type { + case client.LogMessageLine: + writeLogLine(frame.Data) + case client.LogMessageStart: + continue + case client.LogMessageStop: + return nil + case client.LogMessageError: + errorMsg := string(frame.Data) + if errorMsg == "" { + errorMsg = "Log streaming error occurred" + } + return util.NewError(errorMsg) } - }) + } } -// Start starts the log stream handler -func (ls *LogStream) Start() error { - defer ls.cleanup() - - // Monitor log messages - go func() { - for { - select { - case <-ls.ctx.Done(): - return - default: - msg, err := ls.wsClient.ReadMessage() - if err != nil { - // Check if this is a normal closure - if ls.wsClient.IsNormalClosure(err) { - // Normal closure - don't report as error, just exit gracefully - ls.cancel() - return - } - // This is an actual error - format and return - ls.cancel() - return - } - if msg == nil { - // Normal termination (no error, no message) - ls.cancel() - return - } +func writeLogLine(data []byte) { + if len(data) == 0 { + util.WriteStdout([]byte{'\n'}) + return + } + if data[len(data)-1] != '\n' { + data = append(data, '\n') + } + util.WriteStdout(data) +} - // Handle different message types - if msg.IsLogLineMessage() { - // Write log line to stdout - // Ensure the data ends with a newline if it doesn't already - data := msg.Data - if len(data) == 0 { - // Empty line - just write a newline - data = []byte{'\n'} - } else if data[len(data)-1] != '\n' { - // Non-empty line without newline - add one - data = append(data, '\n') - } - // If data already ends with newline, use it as-is - ls.writeToStdout(data) - } else if msg.IsLogStartMessage() { - // Log streaming started - can parse sessionId from data if needed - // For now, just continue - } else if msg.IsLogStopMessage() { - // Log streaming stopped - exit gracefully - ls.cancel() - return - } else if msg.IsLogErrorMessage() { - // Log streaming error - write error and exit - errorMsg := string(msg.Data) - if errorMsg == "" { - errorMsg = "Log streaming error occurred" - } - ls.writeToStdout([]byte(fmt.Sprintf("Error: %s\n", errorMsg))) - ls.cancel() - return - } - } - } - }() +func runRemoteLogStream(clt *client.Client, dial func(*client.Client) (*client.LogSession, error)) error { + util.SpinHandlePrompt() + session, err := dial(clt) + if err != nil { + util.SpinHandlePromptComplete() + return util.NewError(formatLogError(err)) + } - // Check for initial WebSocket errors - if err := ls.wsClient.GetError(); err != nil { - // Check if this is a normal closure - if ls.wsClient.IsNormalClosure(err) { - // Normal closure - don't report as error - ls.cancel() - return nil - } - // This is an actual error - ls.cancel() + if err := streamLogSession(session); err != nil { + util.SpinHandlePromptComplete() return err } - // Wait for context cancellation (stream finished or error) - <-ls.ctx.Done() + util.SpinHandlePromptComplete() return nil } -// formatWebSocketError formats WebSocket errors for better user experience -func formatWebSocketError(err error) string { +func formatLogError(err error) string { if err == nil { return "" } - errStr := err.Error() + switch { + case errors.Is(err, client.ErrLogSessionUnavailable): + return client.ErrLogSessionUnavailable.Error() + case errors.Is(err, client.ErrLogAuthenticationFailed): + return client.ErrLogAuthenticationFailed.Error() + case errors.Is(err, client.ErrAgentNotRunning): + return client.ErrAgentNotRunning.Error() + case errors.Is(err, client.ErrMicroserviceNotRunning): + return client.ErrMicroserviceNotRunning.Error() + case errors.Is(err, client.ErrLogInsufficientPermissions): + return client.ErrLogInsufficientPermissions.Error() + case errors.Is(err, client.ErrLogPolicyViolation): + return client.ErrLogPolicyViolation.Error() + case errors.Is(err, client.ErrLogConnectionLost): + return client.ErrLogConnectionLost.Error() + case errors.Is(err, client.ErrLogMessageTooLarge): + return client.ErrLogMessageTooLarge.Error() + case errors.Is(err, client.ErrLogServerError): + return client.ErrLogServerError.Error() + } - // Handle specific WebSocket error patterns + errStr := err.Error() if strings.Contains(errStr, "close 1008") { - // Extract the reason from the error message if strings.Contains(errStr, "No available log session") { return "No available log session" } @@ -165,26 +109,20 @@ func formatWebSocketError(err error) string { if strings.Contains(errStr, "Insufficient permissions") { return "Insufficient permissions" } - // Default fallback for unknown 1008 errors return "Policy violation: Access denied" } - if strings.Contains(errStr, "close 1006") { return "Connection lost" } - if strings.Contains(errStr, "close 1009") { return "Message too large" } - if strings.Contains(errStr, "close 1011") { return "Server error" } - if strings.Contains(errStr, "failed to connect") { return "Failed to connect to log stream" } - // Default error message return fmt.Sprintf("Log stream error: %v", err) } diff --git a/internal/logs/utils.go b/internal/logs/utils.go index 96a49a138..c8a6191c7 100644 --- a/internal/logs/utils.go +++ b/internal/logs/utils.go @@ -1,23 +1,10 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package logs import ( - "os" + "github.com/eclipse-iofog/iofogctl/pkg/util" ) func printContainerLogs(stdout, stderr string) { - os.Stdout.WriteString(stdout) - os.Stderr.WriteString(stderr) + util.WriteStdoutString(stdout) + util.WriteStderrString(stderr) } diff --git a/internal/move/microservice/executor.go b/internal/move/microservice/executor.go index 469a7dc54..50d78056f 100644 --- a/internal/move/microservice/executor.go +++ b/internal/move/microservice/executor.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package movemicroservice import ( @@ -64,7 +51,7 @@ func Execute(namespace, name, agent string) error { } file := apps.IofogHeader{ - APIVersion: "iofog.org/v3", + APIVersion: util.GetCliApiVersion(), Kind: apps.MicroserviceKind, Metadata: apps.HeaderMetadata{ Name: strings.Join([]string{msvc.Application, msvc.Name}, "/"), diff --git a/internal/prune/agent/execute.go b/internal/prune/agent/execute.go index f53dec4c4..580a0a435 100644 --- a/internal/prune/agent/execute.go +++ b/internal/prune/agent/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package pruneagent import ( @@ -58,7 +45,7 @@ func (exe executor) Execute() error { // Prune Agent switch agent := baseAgent.(type) { case *rsc.LocalAgent: - if err := exe.localAgentPrune(); err != nil { + if err := exe.localAgentPrune(agent); err != nil { return err } case *rsc.RemoteAgent: diff --git a/internal/prune/agent/local.go b/internal/prune/agent/local.go index 703df2d95..4377a6134 100644 --- a/internal/prune/agent/local.go +++ b/internal/prune/agent/local.go @@ -1,37 +1,22 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package pruneagent import ( "fmt" + deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) -func (exe executor) localAgentPrune() error { - containerClient, err := install.NewLocalContainerClient() +func (exe executor) localAgentPrune(agent *rsc.LocalAgent) error { + cfg := deployairgap.EdgeletInstallConfig(deployairgap.LocalEdgeletHostOS(), agent.Config, agent.Package) + edgelet, err := install.NewLocalEdgelet(agent.Name, agent.UUID, cfg) if err != nil { return err } - if _, err = containerClient.ExecuteCmd(install.GetLocalContainerName("agent", false), []string{ - "sudo", - "iofog-agent", - "prune", - }); err != nil { - return util.NewInternalError(fmt.Sprintf("Could not prune local agent. Error: %s\n", err.Error())) + if err := edgelet.Prune(); err != nil { + return util.NewInternalError(fmt.Sprintf("Could not prune local edgelet. Error: %s\n", err.Error())) } - return nil } diff --git a/internal/prune/agent/remote.go b/internal/prune/agent/remote.go index 34c2585c7..f9e3139b2 100644 --- a/internal/prune/agent/remote.go +++ b/internal/prune/agent/remote.go @@ -1,22 +1,10 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package pruneagent import ( "fmt" "strings" + deployairgap "github.com/eclipse-iofog/iofogctl/internal/deploy/airgap" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" @@ -28,8 +16,6 @@ func (exe executor) remoteAgentPrune(agent rsc.Agent) error { if err != nil { return err } - // If controller exists, prune the agent - // Perform Docker pruning of Agent through Controller if err = ctrl.PruneAgent(agent.GetUUID()); err != nil { if !strings.Contains(err.Error(), "NotFoundError") { return err @@ -42,12 +28,13 @@ func (exe executor) remoteDetachedAgentPrune(agent *rsc.RemoteAgent) error { if err := agent.ValidateSSH(); err != nil { return err } - sshAgent, err := install.NewRemoteAgent(agent.SSH.User, agent.Host, agent.SSH.Port, agent.SSH.KeyFile, agent.Name, agent.UUID) + cfg := deployairgap.EdgeletInstallConfig("linux", agent.Config, agent.Package) + edgelet, err := install.NewRemoteEdgelet(agent.SSH.User, agent.Host, agent.SSH.Port, agent.SSH.KeyFile, agent.Name, agent.UUID, cfg) if err != nil { return err } - if err := sshAgent.Prune(); err != nil { - return util.NewInternalError(fmt.Sprintf("Failed to Prune Iofog resource %s. %s", agent.Name, err.Error())) + if err := edgelet.Prune(); err != nil { + return util.NewInternalError(fmt.Sprintf("Failed to Prune edgelet resource %s. %s", agent.Name, err.Error())) } return nil } diff --git a/internal/rebuild/microservice/execute.go b/internal/rebuild/microservice/execute.go index 6a3111bd2..e7ab82a0d 100644 --- a/internal/rebuild/microservice/execute.go +++ b/internal/rebuild/microservice/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package rebuildmicroservice import ( diff --git a/internal/rebuild/systemmicroservice/execute.go b/internal/rebuild/systemmicroservice/execute.go index f2df09580..da68f07d6 100644 --- a/internal/rebuild/systemmicroservice/execute.go +++ b/internal/rebuild/systemmicroservice/execute.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package rebuildsystemmicroservice import ( diff --git a/internal/reconcile/agent/execute.go b/internal/reconcile/agent/execute.go new file mode 100644 index 000000000..4d54fba50 --- /dev/null +++ b/internal/reconcile/agent/execute.go @@ -0,0 +1,27 @@ +package reconcileagent + +import ( + "fmt" + + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type executor struct { + namespace string + name string +} + +func NewExecutor(namespace, name string) execute.Executor { + return &executor{namespace: namespace, name: name} +} + +func (exe *executor) GetName() string { + return exe.name +} + +func (exe *executor) Execute() error { + util.SpinStart(fmt.Sprintf("Reconciling agent %s platform", exe.name)) + return clientutil.ReconcileAgentByNameAndWait(exe.namespace, exe.name) +} diff --git a/internal/reconcile/service/execute.go b/internal/reconcile/service/execute.go new file mode 100644 index 000000000..53719f744 --- /dev/null +++ b/internal/reconcile/service/execute.go @@ -0,0 +1,33 @@ +package reconcileservice + +import ( + "fmt" + + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const serviceHubReadyMessage = "Service hub provisioned; edge bridge listeners on tagged fogs may still be converging." + +type executor struct { + namespace string + name string +} + +func NewExecutor(namespace, name string) execute.Executor { + return &executor{namespace: namespace, name: name} +} + +func (exe *executor) GetName() string { + return exe.name +} + +func (exe *executor) Execute() error { + util.SpinStart(fmt.Sprintf("Reconciling service %s", exe.name)) + if err := clientutil.ReconcileServiceByNameAndWait(exe.namespace, exe.name); err != nil { + return err + } + util.PrintInfo(serviceHubReadyMessage) + return nil +} diff --git a/internal/rename/agent/executor.go b/internal/rename/agent/executor.go deleted file mode 100644 index fc8efb3fc..000000000 --- a/internal/rename/agent/executor.go +++ /dev/null @@ -1,74 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package agent - -import ( - "fmt" - - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - - "github.com/eclipse-iofog/iofogctl/internal/config" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -func Execute(namespace, name, newName string, useDetached bool) error { - if err := util.IsLowerAlphanumeric("Agent", newName); err != nil { - return err - } - util.SpinStart(fmt.Sprintf("Renaming Agent %s", name)) - - if useDetached { - if err := config.RenameDetachedAgent(name, newName); err != nil { - return err - } - return config.Flush() - } - - // Get config - // Update local cache based on Controller - if err := clientutil.SyncAgentInfo(namespace); err != nil { - return err - } - ns, err := config.GetNamespace(namespace) - if err != nil { - return err - } - agent, err := ns.GetAgent(name) - if err != nil { - return err - } - - // Init remote resources - clt, err := clientutil.NewControllerClient(namespace) - if err != nil { - return err - } - - if _, err = clt.UpdateAgent(&client.AgentUpdateRequest{ - UUID: agent.GetUUID(), - Name: newName, - }); err != nil { - return err - } - if err := ns.DeleteAgent(name); err != nil { - return err - } - agent.SetName(newName) - if err := ns.AddAgent(agent); err != nil { - return err - } - - return config.Flush() -} diff --git a/internal/rename/application/executor.go b/internal/rename/application/executor.go deleted file mode 100644 index 9588baee1..000000000 --- a/internal/rename/application/executor.go +++ /dev/null @@ -1,52 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package application - -import ( - "fmt" - - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/internal/config" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -func Execute(namespace, name, newName string) error { - if err := util.IsLowerAlphanumeric("Application", newName); err != nil { - return err - } - util.SpinStart(fmt.Sprintf("Renaming Application %s", name)) - - // Init remote resources - clt, err := clientutil.NewControllerClient(namespace) - if err != nil { - return err - } - - flow, err := clt.GetFlowByName(name) - if err != nil { - return err - } - - flow.Name = newName - _, err = clt.UpdateFlow(&client.FlowUpdateRequest{ - ID: flow.ID, - Name: &newName, - }) - if err != nil { - return err - } - config.Flush() - return nil -} diff --git a/internal/rename/controller/executor.go b/internal/rename/controller/executor.go deleted file mode 100644 index 7e33ae743..000000000 --- a/internal/rename/controller/executor.go +++ /dev/null @@ -1,51 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package controller - -import ( - "fmt" - - "github.com/eclipse-iofog/iofogctl/internal/config" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -func Execute(namespace, name, newName string) error { - ns, err := config.GetNamespace(namespace) - if err != nil { - return err - } - // Check that Controller exists in current namespace - controlPlane, err := ns.GetControlPlane() - if err != nil { - return err - } - - // Get the Controller to rename - controller, err := controlPlane.GetController(name) - if err != nil { - return err - } - - // Check new name is valid - if err := util.IsLowerAlphanumeric("Controller", newName); err != nil { - return err - } - - // Perform the rename - util.SpinStart(fmt.Sprintf("Renaming Controller %s", name)) - controller.SetName(newName) - ns.SetControlPlane(controlPlane) - - return config.Flush() -} diff --git a/internal/rename/edgeresource/executor.go b/internal/rename/edgeresource/executor.go deleted file mode 100644 index 2fd9c7f0f..000000000 --- a/internal/rename/edgeresource/executor.go +++ /dev/null @@ -1,65 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package edgeresource - -import ( - "fmt" - - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -func Execute(namespace, name, newName string) error { - if err := util.IsLowerAlphanumeric("Edge Resource", newName); err != nil { - return err - } - - util.SpinStart(fmt.Sprintf("Renaming edgeResource %s", name)) - - // Init remote resources - clt, err := clientutil.NewControllerClient(namespace) - if err != nil { - return err - } - - // List all edge resources - listResponse, err := clt.ListEdgeResources() - if err != nil { - return err - } - // Validate exists - if len(listResponse.EdgeResources) == 0 { - return util.NewNotFoundError(fmt.Sprintf("%s does not exist", name)) - } - - // Get full resource contents and update - for idx := range listResponse.EdgeResources { - meta := &listResponse.EdgeResources[idx] - if meta.Name != name { - continue - } - // Get versioned resource - oldEdge, err := clt.GetHTTPEdgeResourceByName(meta.Name, meta.Version) - if err != nil { - return err - } - // Update versioned resource - oldEdge.Name = newName - if err := clt.UpdateHTTPEdgeResource(name, &oldEdge); err != nil { - return err - } - } - - return nil -} diff --git a/internal/rename/microservice/executor.go b/internal/rename/microservice/executor.go deleted file mode 100644 index d368ad854..000000000 --- a/internal/rename/microservice/executor.go +++ /dev/null @@ -1,74 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package microservice - -import ( - "bytes" - "fmt" - "strings" - - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/apps" - "github.com/eclipse-iofog/iofogctl/internal/describe" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "gopkg.in/yaml.v2" -) - -func Execute(namespace, name, newName string) error { - if err := util.IsLowerAlphanumeric("Microservice", newName); err != nil { - return err - } - // Init remote resources - clt, err := clientutil.NewControllerClient(namespace) - if err != nil { - return err - } - - appName, msvcName, err := clientutil.ParseFQName(name, "Microservice") - if err != nil { - return err - } - - msvc, err := clt.GetMicroserviceByName(appName, msvcName) - if err != nil { - return err - } - - util.SpinStart(fmt.Sprintf("Renaming microservice %s", name)) - - // Move - msvc.Name = newName - - yamlMsvc, _, _, err := describe.MapClientMicroserviceToDeployMicroservice(msvc, clt) - if err != nil { - return err - } - - file := apps.IofogHeader{ - APIVersion: "iofog.org/v3", - Kind: apps.MicroserviceKind, - Metadata: apps.HeaderMetadata{ - Name: strings.Join([]string{msvc.Application, msvc.Name}, "/"), - }, - Spec: yamlMsvc, - } - yamlBytes, err := yaml.Marshal(file) - if err != nil { - return err - } - - _, err = clt.UpdateMicroserviceFromYAML(msvc.UUID, bytes.NewReader(yamlBytes)) - - return err -} diff --git a/internal/rename/namespace/executor.go b/internal/rename/namespace/executor.go deleted file mode 100644 index 52a76c51d..000000000 --- a/internal/rename/namespace/executor.go +++ /dev/null @@ -1,37 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package namespace - -import ( - "fmt" - - "github.com/eclipse-iofog/iofogctl/internal/config" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -func Execute(name, newName string) error { - if name == "" || name == "default" { - return util.NewError("Cannot rename default or nonexistant namespaces") - } - if err := util.IsLowerAlphanumeric("Namespace", newName); err != nil { - return err - } - - util.SpinStart(fmt.Sprintf("Renaming Namespace %s", name)) - - if err := config.RenameNamespace(name, newName); err != nil { - return err - } - return config.Flush() -} diff --git a/internal/resource/agent.go b/internal/resource/agent.go index ee5d197ab..2e19bec99 100644 --- a/internal/resource/agent.go +++ b/internal/resource/agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource type Agent interface { diff --git a/internal/resource/agent_validate.go b/internal/resource/agent_validate.go new file mode 100644 index 000000000..466803bd0 --- /dev/null +++ b/internal/resource/agent_validate.go @@ -0,0 +1,21 @@ +package resource + +import ( + "fmt" + + "github.com/eclipse-iofog/iofogctl/pkg/iofog" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func validateSystemAgentRouterNats(label string, cfg *AgentConfiguration) error { + if cfg == nil { + return nil + } + if cfg.RouterMode != nil && *cfg.RouterMode != iofog.RouterModeInterior { + return util.NewInputError(fmt.Sprintf("%s systemAgent.config.routerMode must be %q when set", label, iofog.RouterModeInterior)) + } + if cfg.NatsMode != nil && *cfg.NatsMode != iofog.NatsModeServer { + return util.NewInputError(fmt.Sprintf("%s systemAgent.config.natsMode must be %q when set", label, iofog.NatsModeServer)) + } + return nil +} diff --git a/internal/resource/console_url.go b/internal/resource/console_url.go new file mode 100644 index 000000000..763644c56 --- /dev/null +++ b/internal/resource/console_url.go @@ -0,0 +1,212 @@ +package resource + +import ( + "net/url" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const consoleHostPort = "80" + +// ResolveConsoleURL returns the EdgeOps Console URL for a control plane. +// Primary source is spec.controller.consoleUrl. When empty, fall back to +// publicUrl, cp.Endpoint, then controllers[0].Endpoint. API-style host:port +// endpoints are mapped to the console binding on host port 80. +func ResolveConsoleURL(cp ControlPlane) (string, error) { + if cp == nil { + return "", util.NewInternalError("Control Plane is nil") + } + + if consoleURL := strings.TrimSpace(controllerConsoleURL(cp)); consoleURL != "" { + return consoleURL, nil + } + + for _, candidate := range consoleURLFallbackCandidates(cp) { + useHTTPS := controlPlanePrefersHTTPS(cp, candidate) + candidate = strings.TrimSpace(candidate) + if candidate == "" { + continue + } + resolved, err := resolveConsoleFallback(candidate, useHTTPS) + if err != nil { + return "", err + } + if resolved != "" { + return resolved, nil + } + } + + return "", util.NewError("Control Plane does not have a console URL") +} + +// BackfillConsoleURL sets spec.controller.consoleUrl when it is empty, using ResolveConsoleURL. +func BackfillConsoleURL(cp ControlPlane) error { + resolved, err := ResolveConsoleURL(cp) + if err != nil { + return err + } + switch c := cp.(type) { + case *KubernetesControlPlane: + if strings.TrimSpace(c.Controller.ConsoleUrl) == "" { + c.Controller.ConsoleUrl = resolved + } + case *LocalControlPlane: + if strings.TrimSpace(c.Controller.ConsoleUrl) == "" { + c.Controller.ConsoleUrl = resolved + } + case *RemoteControlPlane: + if strings.TrimSpace(c.Controller.ConsoleUrl) == "" { + c.Controller.ConsoleUrl = resolved + } + } + return nil +} + +func controllerConsoleURL(cp ControlPlane) string { + switch c := cp.(type) { + case *KubernetesControlPlane: + return c.Controller.ConsoleUrl + case *LocalControlPlane: + return c.Controller.ConsoleUrl + case *RemoteControlPlane: + return c.Controller.ConsoleUrl + default: + return "" + } +} + +func controllerPublicURL(cp ControlPlane) string { + switch c := cp.(type) { + case *KubernetesControlPlane: + return c.Controller.PublicUrl + case *LocalControlPlane: + return c.Controller.PublicUrl + case *RemoteControlPlane: + return c.Controller.PublicUrl + default: + return "" + } +} + +func controlPlaneStoredEndpoint(cp ControlPlane) string { + switch c := cp.(type) { + case *KubernetesControlPlane: + return c.Endpoint + case *LocalControlPlane: + return c.Endpoint + case *RemoteControlPlane: + return c.Endpoint + default: + return "" + } +} + +func consoleURLFallbackCandidates(cp ControlPlane) []string { + candidates := []string{ + controllerPublicURL(cp), + controlPlaneStoredEndpoint(cp), + } + controllers := cp.GetControllers() + if len(controllers) > 0 { + candidates = append(candidates, controllers[0].GetEndpoint()) + } + return candidates +} + +func controlPlanePrefersHTTPS(cp ControlPlane, rawURL string) bool { + if scheme, ok := urlScheme(rawURL); ok { + return strings.EqualFold(scheme, "https") + } + + switch c := cp.(type) { + case *KubernetesControlPlane: + if c.Controller.Https != nil && *c.Controller.Https { + return true + } + case *LocalControlPlane: + if c.Controller.Https != nil && *c.Controller.Https { + return true + } + if tlsEnabled(c.TLS) { + return true + } + case *RemoteControlPlane: + if c.Controller.Https != nil && *c.Controller.Https { + return true + } + if tlsEnabled(c.TLS) { + return true + } + for idx := range c.Controllers { + if tlsEnabled(c.Controllers[idx].TLS) { + return true + } + } + } + + return false +} + +func tlsEnabled(tls *ControlPlaneTLS) bool { + return tls != nil && strings.TrimSpace(tls.Cert) != "" && strings.TrimSpace(tls.Key) != "" +} + +func resolveConsoleFallback(raw string, useHTTPS bool) (string, error) { + u, err := parseLooseURL(raw) + if err != nil { + return "", err + } + if u.Host == "" { + return "", nil + } + + if !hasNonConsolePort(u) { + if u.Scheme == "" { + if useHTTPS { + u.Scheme = "https" + } else { + u.Scheme = "http" + } + } + return u.String(), nil + } + + host := u.Hostname() + if useHTTPS || strings.EqualFold(u.Scheme, "https") { + return "https://" + host + ":" + consoleHostPort, nil + } + return "http://" + host, nil +} + +func hasNonConsolePort(u *url.URL) bool { + port := u.Port() + if port == "" { + return false + } + return port != consoleHostPort +} + +func parseLooseURL(raw string) (*url.URL, error) { + u, err := url.Parse(raw) + if err != nil || u.Host == "" { + host := raw + if !strings.Contains(host, "://") && !strings.Contains(host, ":") { + host = host + ":" + client.ControllerPortString + } + u, err = url.Parse("//" + host) + if err != nil { + return nil, err + } + } + return u, nil +} + +func urlScheme(raw string) (string, bool) { + u, err := url.Parse(raw) + if err != nil || u.Scheme == "" { + return "", false + } + return u.Scheme, true +} diff --git a/internal/resource/console_url_test.go b/internal/resource/console_url_test.go new file mode 100644 index 000000000..3d193ab53 --- /dev/null +++ b/internal/resource/console_url_test.go @@ -0,0 +1,191 @@ +package resource + +import ( + "testing" +) + +func TestResolveConsoleURLExplicitConsole(t *testing.T) { + cp := &LocalControlPlane{ + Controller: LocalControllerSpec{ + ControllerConfig: ControllerConfig{ + ConsoleUrl: "http://192.168.1.6", + PublicUrl: "http://192.168.1.6:51121", + }, + }, + Endpoint: "http://192.168.1.6:51121", + } + + got, err := ResolveConsoleURL(cp) + if err != nil { + t.Fatal(err) + } + if got != "http://192.168.1.6" { + t.Fatalf("got %q, want %q", got, "http://192.168.1.6") + } +} + +func TestResolveConsoleURLRemoteFixture(t *testing.T) { + cp := &RemoteControlPlane{ + Controller: LocalControllerSpec{ + ControllerConfig: ControllerConfig{ + ConsoleUrl: "https://192.168.105.2:80", + PublicUrl: "https://192.168.105.2:51121", + }, + }, + Endpoint: "https://192.168.105.2:51121", + } + + got, err := ResolveConsoleURL(cp) + if err != nil { + t.Fatal(err) + } + if got != "https://192.168.105.2:80" { + t.Fatalf("got %q, want %q", got, "https://192.168.105.2:80") + } +} + +func TestResolveConsoleURLHTTPAPIFallback(t *testing.T) { + cp := &LocalControlPlane{ + Controller: LocalControllerSpec{ + ControllerConfig: ControllerConfig{ + PublicUrl: "http://192.168.1.6:51121", + }, + }, + Endpoint: "http://192.168.1.6:51121", + Controllers: []LocalController{{ + Name: "iofog", + Endpoint: "http://192.168.1.6:51121", + }}, + } + + got, err := ResolveConsoleURL(cp) + if err != nil { + t.Fatal(err) + } + if got != "http://192.168.1.6" { + t.Fatalf("got %q, want %q", got, "http://192.168.1.6") + } +} + +func TestResolveConsoleURLHTTPSAPIFallback(t *testing.T) { + https := true + cp := &RemoteControlPlane{ + TLS: &ControlPlaneTLS{Cert: "cert", Key: "key"}, + Controller: LocalControllerSpec{ + ControllerConfig: ControllerConfig{ + PublicUrl: "https://10.0.0.5:51121", + }, + }, + Endpoint: "https://10.0.0.5:51121", + Controllers: []RemoteController{{ + Name: "controlplane", + Endpoint: "https://10.0.0.5:51121", + }}, + } + cp.Controller.Https = &https + + got, err := ResolveConsoleURL(cp) + if err != nil { + t.Fatal(err) + } + if got != "https://10.0.0.5:80" { + t.Fatalf("got %q, want %q", got, "https://10.0.0.5:80") + } +} + +func TestResolveConsoleURLLocalhostAPIFallback(t *testing.T) { + cp := &LocalControlPlane{ + Endpoint: "http://localhost:51121", + Controllers: []LocalController{{ + Name: "local", + Endpoint: "http://localhost:51121", + }}, + } + + got, err := ResolveConsoleURL(cp) + if err != nil { + t.Fatal(err) + } + if got != "http://localhost" { + t.Fatalf("got %q, want %q", got, "http://localhost") + } +} + +func TestResolveConsoleURLK8sIngressHostname(t *testing.T) { + cp := &KubernetesControlPlane{ + Controller: ControllerConfig{ + PublicUrl: "https://controller.example.com", + ConsoleUrl: "https://console.example.com", + }, + Endpoint: "https://controller.example.com", + } + + got, err := ResolveConsoleURL(cp) + if err != nil { + t.Fatal(err) + } + if got != "https://console.example.com" { + t.Fatalf("got %q, want %q", got, "https://console.example.com") + } +} + +func TestResolveConsoleURLK8sFallbackWithoutPort(t *testing.T) { + cp := &KubernetesControlPlane{ + Controller: ControllerConfig{ + PublicUrl: "https://controller.example.com", + }, + Endpoint: "https://controller.example.com", + } + + got, err := ResolveConsoleURL(cp) + if err != nil { + t.Fatal(err) + } + if got != "https://controller.example.com" { + t.Fatalf("got %q, want %q", got, "https://controller.example.com") + } +} + +func TestResolveConsoleURLConnectOnlyEndpoint(t *testing.T) { + cp := &RemoteControlPlane{ + Endpoint: "http://203.0.113.10:51121", + Controllers: []RemoteController{{ + Name: "remote", + Endpoint: "http://203.0.113.10:51121", + }}, + } + + got, err := ResolveConsoleURL(cp) + if err != nil { + t.Fatal(err) + } + if got != "http://203.0.113.10" { + t.Fatalf("got %q, want %q", got, "http://203.0.113.10") + } +} + +func TestBackfillConsoleURL(t *testing.T) { + cp := &LocalControlPlane{ + Controller: LocalControllerSpec{ + ControllerConfig: ControllerConfig{ + PublicUrl: "http://192.168.1.6:51121", + }, + }, + Endpoint: "http://192.168.1.6:51121", + } + + if err := BackfillConsoleURL(cp); err != nil { + t.Fatal(err) + } + if cp.Controller.ConsoleUrl != "http://192.168.1.6" { + t.Fatalf("ConsoleUrl = %q, want %q", cp.Controller.ConsoleUrl, "http://192.168.1.6") + } + + original := cp.Controller.ConsoleUrl + if err := BackfillConsoleURL(cp); err != nil { + t.Fatal(err) + } + if cp.Controller.ConsoleUrl != original { + t.Fatalf("BackfillConsoleURL overwrote explicit value: %q", cp.Controller.ConsoleUrl) + } +} diff --git a/internal/resource/controller.go b/internal/resource/controller.go index cbeafe453..c2046cae8 100644 --- a/internal/resource/controller.go +++ b/internal/resource/controller.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource type Controller interface { diff --git a/internal/resource/controlplane.go b/internal/resource/controlplane.go index fb80109c9..039f0eb59 100644 --- a/internal/resource/controlplane.go +++ b/internal/resource/controlplane.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource type ControlPlane interface { @@ -19,6 +6,7 @@ type ControlPlane interface { GetControllers() []Controller GetController(string) (Controller, error) GetEndpoint() (string, error) + GetTrustCA() string UpdateController(Controller) error AddController(Controller) error DeleteController(string) error diff --git a/internal/resource/controlplane_test.go b/internal/resource/controlplane_test.go index 17d98399d..8e12a9b4b 100644 --- a/internal/resource/controlplane_test.go +++ b/internal/resource/controlplane_test.go @@ -1,21 +1,10 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource import ( - "github.com/eclipse-iofog/iofogctl/pkg/util" + "strings" "testing" + + "gopkg.in/yaml.v2" ) const ( @@ -23,11 +12,59 @@ const ( password = "as901yh3rinsd" ) +func TestKubernetesControlPlaneYAMLNoEcnViewer(t *testing.T) { + trustProxy := true + cp := KubernetesControlPlane{ + Endpoint: "https://controller.example.com", + KubeConfig: "/tmp/kubeconfig", + IofogUser: IofogUser{Email: email}, + Controller: ControllerConfig{ + PublicUrl: "https://controller.example.com", + ConsoleUrl: "https://controller.example.com", + TrustProxy: &trustProxy, + }, + } + out, err := yaml.Marshal(cp) + if err != nil { + t.Fatal(err) + } + text := string(out) + for _, retired := range []string{"ecnViewerPort", "ecnViewerUrl"} { + if strings.Contains(text, retired) { + t.Fatalf("YAML must not contain %q:\n%s", retired, text) + } + } + for _, want := range []string{"endpoint:", "publicUrl:", "consoleUrl:"} { + if !strings.Contains(text, want) { + t.Fatalf("YAML missing %q:\n%s", want, text) + } + } +} + func TestKubernetesControlPlane(t *testing.T) { + trustProxy := true cp := KubernetesControlPlane{ Endpoint: "123.123.123.123", KubeConfig: "~/.kube/config", IofogUser: IofogUser{Email: "user@domain.com", Password: "password"}, + Controller: ControllerConfig{ + PublicUrl: "https://controller.example.com", + ConsoleUrl: "https://console.example.com", + TrustProxy: &trustProxy, + }, + Auth: Auth{ + Mode: "embedded", + Bootstrap: &AuthBootstrap{ + Username: "admin", + Password: "BootstrapPass1!", + }, + }, + } + if cp.Controller.PublicUrl != "https://controller.example.com" { + t.Error("Wrong publicUrl") + } + if cp.Auth.Mode != "embedded" { + t.Error("Wrong auth mode") } if endpoint, err := cp.GetEndpoint(); err != nil || endpoint != "123.123.123.123" { t.Error("Wrong endpoint") @@ -172,8 +209,25 @@ func TestRemoteControlPlane(t *testing.T) { } func TestLocalControlPlane(t *testing.T) { + arch := "amd64" cp := LocalControlPlane{ + Endpoint: "https://controller.example.com", IofogUser: IofogUser{Email: "user@domain.com", Password: "password"}, + Controller: LocalControllerSpec{ + ControllerConfig: ControllerConfig{ + PublicUrl: "https://controller.example.com", + }, + }, + Auth: Auth{ + Mode: "embedded", + Bootstrap: &AuthBootstrap{ + Username: "admin", + Password: "BootstrapPass1!", + }, + }, + SystemAgent: &SystemAgentConfig{ + AgentConfiguration: &AgentConfiguration{Arch: &arch}, + }, } if err := cp.AddController(&LocalController{ Name: "ctrl1", @@ -181,9 +235,9 @@ func TestLocalControlPlane(t *testing.T) { }); err != nil { t.Error(err) } - cp.Sanitize() + _ = cp.Sanitize() - if endpoint, err := cp.GetEndpoint(); err != nil || !util.IsLocalHost(endpoint) { + if endpoint, err := cp.GetEndpoint(); err != nil || endpoint != "https://controller.example.com" { t.Errorf("Wrong endpoint: %s", endpoint) } if user := cp.GetUser(); user.Email != "user@domain.com" || user.Password != "password" { @@ -206,3 +260,29 @@ func TestLocalControlPlane(t *testing.T) { t.Error("Should have returned error when getting Local Controller") } } + +func TestLocalControlPlaneControllersPersistInYAML(t *testing.T) { + cp := LocalControlPlane{ + Endpoint: "http://192.168.1.6:51121", + Controllers: []LocalController{{ + Name: "iofog", + Endpoint: "http://192.168.1.6:51121", + Created: "2026-06-22T22:49:47.739Z", + }}, + } + data, err := yaml.Marshal(cp) + if err != nil { + t.Fatal(err) + } + var loaded LocalControlPlane + if err := yaml.UnmarshalStrict(data, &loaded); err != nil { + t.Fatal(err) + } + if len(loaded.GetControllers()) != 1 { + t.Fatalf("controller count = %d, want 1", len(loaded.GetControllers())) + } + ctrl := loaded.GetControllers()[0] + if ctrl.GetName() != "iofog" || ctrl.GetEndpoint() != "http://192.168.1.6:51121" { + t.Fatalf("unexpected controller record: %+v", ctrl) + } +} diff --git a/internal/resource/cpv3.go b/internal/resource/cpv3.go new file mode 100644 index 000000000..cb2cb6efb --- /dev/null +++ b/internal/resource/cpv3.go @@ -0,0 +1,77 @@ +package resource + +import ( + cpv3 "github.com/eclipse-iofog/iofog-operator/v3/apis/controlplanes/v3" +) + +// AuthToCPV3 converts CLI resource auth to operator ControlPlane auth (3D translator helper). +func AuthToCPV3(a Auth) cpv3.Auth { + out := cpv3.Auth{ + Mode: cpv3.AuthMode(a.Mode), + InsecureAllowHttp: a.InsecureAllowHttp, + InsecureAllowBootstrapLog: a.InsecureAllowBootstrapLog, + IssuerUrl: a.IssuerUrl, + ConsoleClient: a.ConsoleClient, + ConsoleClientEnabled: a.ConsoleClientEnabled, + } + if a.Bootstrap != nil { + out.Bootstrap = &cpv3.AuthBootstrap{ + Username: a.Bootstrap.Username, + Password: a.Bootstrap.Password, + } + } + if a.Client != nil { + out.Client = &cpv3.AuthClient{ + ID: a.Client.ID, + Secret: a.Client.Secret, + } + } + if a.RateLimit != nil { + out.RateLimit = &cpv3.AuthRateLimit{ + Enabled: a.RateLimit.Enabled, + MaxRequestsPerWindow: a.RateLimit.MaxRequestsPerWindow, + WindowMs: a.RateLimit.WindowMs, + } + } + if a.SessionStore != nil { + out.SessionStore = &cpv3.AuthSessionStore{ + Type: a.SessionStore.Type, + TtlMs: a.SessionStore.TtlMs, + Secret: a.SessionStore.Secret, + } + } + if a.TokenTtl != nil { + out.TokenTtl = &cpv3.AuthTokenTtl{ + AccessTokenTtlSeconds: a.TokenTtl.AccessTokenTtlSeconds, + RefreshTokenTtlSeconds: a.TokenTtl.RefreshTokenTtlSeconds, + } + } + if a.OidcTtl != nil { + out.OidcTtl = &cpv3.AuthOidcTtl{ + InteractionTtlSeconds: a.OidcTtl.InteractionTtlSeconds, + GrantTtlSeconds: a.OidcTtl.GrantTtlSeconds, + SessionTtlSeconds: a.OidcTtl.SessionTtlSeconds, + IdTokenTtlSeconds: a.OidcTtl.IdTokenTtlSeconds, + } + } + return out +} + +// ControllerConfigToCPV3 converts spec.controller to operator Controller (3D translator helper). +func ControllerConfigToCPV3(c ControllerConfig) cpv3.Controller { + https := c.Https + if https == nil { + defaultHTTPS := false + https = &defaultHTTPS + } + return cpv3.Controller{ + PublicUrl: c.PublicUrl, + TrustProxy: c.TrustProxy, + ConsoleUrl: c.ConsoleUrl, + ConsolePort: c.ConsolePort, + PidBaseDir: c.PidBaseDir, + Https: https, + SecretName: c.SecretName, + LogLevel: c.LogLevel, + } +} diff --git a/internal/resource/edgelet_golden_test.go b/internal/resource/edgelet_golden_test.go new file mode 100644 index 000000000..bc5334ffd --- /dev/null +++ b/internal/resource/edgelet_golden_test.go @@ -0,0 +1,162 @@ +package resource + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func edgeletFixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "testdata", "edgelet", name) +} + +func loadEdgeletFixture(t *testing.T, name string) []byte { + t.Helper() + data, err := os.ReadFile(edgeletFixturePath(name)) + require.NoError(t, err) + return data +} + +func strPtr(v string) *string { return &v } + +func assertGoldenAgentConfig(t *testing.T, cfg *AgentConfiguration) { + t.Helper() + require.NotNil(t, cfg) + require.Equal(t, "edgelet running on device", cfg.Description) + require.Equal(t, 46.204391, cfg.Latitude) + require.Equal(t, 6.143158, cfg.Longitude) + require.NotNil(t, cfg.Arch) + require.Equal(t, "amd64", *cfg.Arch) + require.NotNil(t, cfg.DeploymentType) + require.Equal(t, "native", *cfg.DeploymentType) + require.NotNil(t, cfg.ContainerEngine) + require.Equal(t, "edgelet", *cfg.ContainerEngine) + require.NotNil(t, cfg.ContainerEngineURL) + require.Equal(t, "unix:///run/edgelet/containerd.sock", *cfg.ContainerEngineURL) + require.NotNil(t, cfg.DiskLimit) + require.Equal(t, int64(50), *cfg.DiskLimit) + require.NotNil(t, cfg.RouterMode) + require.Equal(t, "edge", *cfg.RouterMode) + require.NotNil(t, cfg.MessagingPort) + require.Equal(t, 5671, *cfg.MessagingPort) + require.NotNil(t, cfg.NatsMode) + require.Equal(t, "leaf", *cfg.NatsMode) + require.NotNil(t, cfg.UpstreamNatsServers) + require.Equal(t, []string{"default-nats-hub"}, *cfg.UpstreamNatsServers) +} + +func TestGoldenUnmarshalRemoteAgent(t *testing.T) { + raw := loadEdgeletFixture(t, "remote-agent-spec.yaml") + agent, err := UnmarshallRemoteAgent(raw) + require.NoError(t, err) + require.Equal(t, "30.40.50.6", agent.Host) + require.Equal(t, "foo", agent.SSH.User) + require.Contains(t, agent.SSH.KeyFile, "id_rsa") + require.Equal(t, 22, agent.SSH.Port) + require.NotNil(t, agent.Config) + assertGoldenAgentConfig(t, agent.Config) +} + +func TestGoldenUnmarshalRemoteAgentPackageRegistry(t *testing.T) { + raw := loadEdgeletFixture(t, "remote-agent-package-registry.yaml") + agent, err := UnmarshallRemoteAgent(raw) + require.NoError(t, err) + require.True(t, agent.Airgap) + require.Equal(t, "1.0.0-rc.6", agent.Package.Version) + require.Equal(t, "ghcr.io/datasance/edgelet:1.0.0-rc.6", agent.Package.Container.Image) + require.Equal(t, "ghcr.io", agent.Package.Container.Registry) + require.Equal(t, "foo", agent.Package.Container.Username) + require.Equal(t, "bar", agent.Package.Container.Password) + require.NotNil(t, agent.Scripts) + require.Equal(t, "/tmp/my-scripts", agent.Scripts.Directory) + require.Equal(t, "install_deps.sh", agent.Scripts.Deps.Name) + require.Equal(t, "install.sh", agent.Scripts.Install.Name) + require.Equal(t, []string{"1.0.0-rc.6"}, agent.Scripts.Install.Args) + require.Equal(t, "uninstall.sh", agent.Scripts.Uninstall.Name) +} + +func TestGoldenUnmarshalLocalAgent(t *testing.T) { + raw := loadEdgeletFixture(t, "local-agent-spec.yaml") + agent, err := UnmarshallLocalAgent(raw) + require.NoError(t, err) + require.Equal(t, "local", agent.Name) + require.Equal(t, "1.0.0-rc.6", agent.Package.Version) + require.Equal(t, "ghcr.io/datasance/edgelet:1.0.0-rc.6", agent.Package.Container.Image) + require.NotNil(t, agent.Config) + assertGoldenAgentConfig(t, agent.Config) + require.Equal(t, "30.40.50.6", agent.GetHost()) +} + +func TestGoldenUnmarshalAgentConfiguration(t *testing.T) { + raw := loadEdgeletFixture(t, "agent-config-spec.yaml") + cfg, err := UnmarshallAgentConfiguration(raw) + require.NoError(t, err) + require.Equal(t, "agent running on VM", cfg.Description) + require.Equal(t, 46.204391, cfg.Latitude) + require.NotNil(t, cfg.Arch) + require.Equal(t, "riscv64", *cfg.Arch) + require.NotNil(t, cfg.ContainerEngine) + require.Equal(t, "edgelet", *cfg.ContainerEngine) + require.NotNil(t, cfg.RouterMode) + require.Equal(t, "edge", *cfg.RouterMode) + require.NotNil(t, cfg.NatsMode) + require.Equal(t, "leaf", *cfg.NatsMode) + require.NotNil(t, cfg.LogLevel) + require.Equal(t, "INFO", *cfg.LogLevel) +} + +func TestGoldenRoundTripRemoteAgent(t *testing.T) { + raw := loadEdgeletFixture(t, "remote-agent-spec.yaml") + agent, err := UnmarshallRemoteAgent(raw) + require.NoError(t, err) + + out, err := yaml.Marshal(&agent) + require.NoError(t, err) + + var round RemoteAgent + require.NoError(t, yaml.UnmarshalStrict(out, &round)) + require.Equal(t, agent.Host, round.Host) + require.Equal(t, agent.SSH, round.SSH) + require.Equal(t, *agent.Config.Arch, *round.Config.Arch) +} + +func TestArchStringToID(t *testing.T) { + id, ok := ArchStringToID("amd64") + require.True(t, ok) + require.Equal(t, int64(1), id) + + id, ok = ArchStringToID("riscv64") + require.True(t, ok) + require.Equal(t, int64(3), id) + + _, ok = ArchStringToID("x86") + require.False(t, ok) +} + +func TestArchIDToString(t *testing.T) { + name, ok := ArchIDToString(2) + require.True(t, ok) + require.Equal(t, "arm64", name) + + name, ok = ArchIDToString(0) + require.True(t, ok) + require.Equal(t, "auto", name) +} + +func TestLocalAgentGetHostFallback(t *testing.T) { + agent := &LocalAgent{} + require.Equal(t, "localhost", agent.GetHost()) + + agent.Host = "192.168.1.10" + require.Equal(t, "192.168.1.10", agent.GetHost()) + + host := "10.0.0.5" + agent.Config = &AgentConfiguration{} + agent.Config.Host = &host + require.Equal(t, "10.0.0.5", agent.GetHost()) +} diff --git a/internal/resource/k8s_controller.go b/internal/resource/k8s_controller.go index 9857bd772..441361208 100644 --- a/internal/resource/k8s_controller.go +++ b/internal/resource/k8s_controller.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource type KubernetesController struct { diff --git a/internal/resource/k8s_controlplane.go b/internal/resource/k8s_controlplane.go index ea0189e33..ce9ad6eb2 100644 --- a/internal/resource/k8s_controlplane.go +++ b/internal/resource/k8s_controlplane.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource import ( @@ -21,6 +8,7 @@ import ( type KubernetesControlPlane struct { KubeConfig string `yaml:"config"` + CA string `yaml:"ca,omitempty"` IofogUser IofogUser `yaml:"iofogUser"` ControllerPods []KubernetesController `yaml:"controllerPods,omitempty"` Database Database `yaml:"database"` @@ -30,12 +18,16 @@ type KubernetesControlPlane struct { Replicas Replicas `yaml:"replicas,omitempty"` Images KubeImages `yaml:"images,omitempty"` Endpoint string `yaml:"endpoint,omitempty"` - Controller K8SControllerConfig `yaml:"controller,omitempty"` + Controller ControllerConfig `yaml:"controller,omitempty"` Ingresses Ingresses `yaml:"ingresses,omitempty"` Nats *NatsSpec `yaml:"nats,omitempty"` Vault *VaultSpec `yaml:"vault,omitempty"` } +func (cp *KubernetesControlPlane) GetTrustCA() string { + return cp.CA +} + func (cp *KubernetesControlPlane) GetUser() IofogUser { return cp.IofogUser } @@ -144,6 +136,7 @@ func (cp *KubernetesControlPlane) Clone() ControlPlane { copy(controllerPods, cp.ControllerPods) return &KubernetesControlPlane{ KubeConfig: cp.KubeConfig, + CA: cp.CA, IofogUser: cp.IofogUser, Auth: cp.Auth, Database: cp.Database, diff --git a/internal/resource/k8s_controlplane_golden_test.go b/internal/resource/k8s_controlplane_golden_test.go new file mode 100644 index 000000000..2d46d9d74 --- /dev/null +++ b/internal/resource/k8s_controlplane_golden_test.go @@ -0,0 +1,145 @@ +package resource + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func fixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "testdata", "k8s", name) +} + +func loadFixture(t *testing.T, name string) []byte { + t.Helper() + data, err := os.ReadFile(fixturePath(name)) + require.NoError(t, err) + return data +} + +func assertGoldenControlPlane(t *testing.T, cp *KubernetesControlPlane) { + t.Helper() + require.True(t, strings.HasSuffix(cp.KubeConfig, ".kube/config"), "kubeconfig path: %s", cp.KubeConfig) + require.Equal(t, "Foo", cp.IofogUser.Name) + require.Equal(t, "Bar", cp.IofogUser.Surname) + require.Equal(t, email, cp.IofogUser.Email) + require.Equal(t, int32(2), cp.Replicas.Controller) + require.Equal(t, int32(2), cp.Replicas.Nats) + require.Equal(t, "https://controller.example.com", cp.Controller.PublicUrl) + require.NotNil(t, cp.Controller.TrustProxy) + require.True(t, *cp.Controller.TrustProxy) + require.Equal(t, 8080, cp.Controller.ConsolePort) + require.Equal(t, "https://controller.example.com", cp.Controller.ConsoleUrl) + require.Equal(t, "info", cp.Controller.LogLevel) + require.Equal(t, "embedded", cp.Auth.Mode) + require.NotNil(t, cp.Auth.Bootstrap) + require.Equal(t, "admin", cp.Auth.Bootstrap.Username) + require.NotNil(t, cp.Events.AuditEnabled) + require.True(t, *cp.Events.AuditEnabled) + require.Equal(t, 14, cp.Events.RetentionDays) + require.Equal(t, 86400, cp.Events.CleanupInterval) + require.NotNil(t, cp.Events.CaptureIpAddress) + require.True(t, *cp.Events.CaptureIpAddress) + require.NotEmpty(t, cp.Images.Controller) + require.NotEmpty(t, cp.Images.Router) + require.NotEmpty(t, cp.Images.Nats) + require.NotEmpty(t, cp.Images.Operator) + require.NotNil(t, cp.Nats) + require.NotNil(t, cp.Nats.Enabled) + require.True(t, *cp.Nats.Enabled) + require.Equal(t, "10Gi", cp.Nats.JetStream.StorageSize) + require.Equal(t, "nginx", cp.Ingresses.Controller.IngressClassName) + require.Equal(t, "controller.example.com", cp.Ingresses.Controller.Host) + require.Equal(t, 5671, cp.Ingresses.Router.MessagePort) + require.Equal(t, 4222, cp.Ingresses.Nats.ServerPort) +} + +func TestGoldenUnmarshalKubernetesControlPlane_WithCA(t *testing.T) { + raw := loadFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallKubernetesControlPlane(append([]byte("ca: dGVzdC1jYQ==\n"), raw...)) + require.NoError(t, err) + require.Equal(t, "dGVzdC1jYQ==", cp.GetTrustCA()) + require.Equal(t, "dGVzdC1jYQ==", GetTrustCA(&cp)) +} + +func TestGoldenUnmarshalKubernetesControlPlane_Datasance(t *testing.T) { + raw := loadFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallKubernetesControlPlane(raw) + require.NoError(t, err) + assertGoldenControlPlane(t, &cp) +} + +func TestGoldenUnmarshalKubernetesControlPlane_Iofog(t *testing.T) { + raw := loadFixture(t, "controlplane-iofog.yaml") + cp, err := UnmarshallKubernetesControlPlane(raw) + require.NoError(t, err) + assertGoldenControlPlane(t, &cp) +} + +func TestGoldenRoundTripKubernetesControlPlane(t *testing.T) { + raw := loadFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallKubernetesControlPlane(raw) + require.NoError(t, err) + + out, err := yaml.Marshal(&cp) + require.NoError(t, err) + + var round KubernetesControlPlane + require.NoError(t, yaml.UnmarshalStrict(out, &round)) + require.Equal(t, cp.KubeConfig, round.KubeConfig) + require.Equal(t, cp.Controller.PublicUrl, round.Controller.PublicUrl) + require.Equal(t, cp.Auth.Mode, round.Auth.Mode) + require.Equal(t, cp.Replicas, round.Replicas) +} + +func TestKubernetesControlPlane_GetTrustCA(t *testing.T) { + cp := KubernetesControlPlane{CA: "dGVzdC1jYQ=="} + require.Equal(t, "dGVzdC1jYQ==", cp.GetTrustCA()) + require.Equal(t, "dGVzdC1jYQ==", GetTrustCA(&cp)) +} + +func TestLocalControlPlane_GetTrustCA(t *testing.T) { + cp := LocalControlPlane{CA: "dGVzdC1jYQ=="} + require.Equal(t, "dGVzdC1jYQ==", cp.GetTrustCA()) + require.Equal(t, "dGVzdC1jYQ==", GetTrustCA(&cp)) +} + +func TestRemoteControlPlane_GetTrustCA(t *testing.T) { + cp := RemoteControlPlane{CA: "dGVzdC1jYQ=="} + require.Equal(t, "dGVzdC1jYQ==", cp.GetTrustCA()) + require.Equal(t, "dGVzdC1jYQ==", GetTrustCA(&cp)) +} + +func TestAuthToCPV3(t *testing.T) { + insecure := false + cp := KubernetesControlPlane{ + Auth: Auth{ + Mode: "embedded", + InsecureAllowHttp: &insecure, + Bootstrap: &AuthBootstrap{Username: "admin", Password: "secret"}, + }, + } + out := AuthToCPV3(cp.Auth) + require.Equal(t, "embedded", string(out.Mode)) + require.NotNil(t, out.Bootstrap) + require.Equal(t, "admin", out.Bootstrap.Username) +} + +func TestControllerConfigToCPV3(t *testing.T) { + trust := true + cfg := ControllerConfig{ + PublicUrl: "https://controller.example.com", + TrustProxy: &trust, + ConsoleUrl: "https://console.example.com", + } + out := ControllerConfigToCPV3(cfg) + require.Equal(t, cfg.PublicUrl, out.PublicUrl) + require.Equal(t, cfg.ConsoleUrl, out.ConsoleUrl) + require.True(t, *out.TrustProxy) +} diff --git a/internal/resource/local_agent.go b/internal/resource/local_agent.go index 4d1464be2..8935c6914 100644 --- a/internal/resource/local_agent.go +++ b/internal/resource/local_agent.go @@ -1,26 +1,15 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource type LocalAgent struct { Name string `yaml:"name,omitempty"` UUID string `yaml:"uuid,omitempty"` - Container Container `yaml:"container,omitempty"` Created string `yaml:"created,omitempty"` Host string `yaml:"host,omitempty"` + Package Package `yaml:"package,omitempty"` Config *AgentConfiguration `yaml:"config,omitempty"` + Scripts *AgentScripts `yaml:"scripts,omitempty"` ControllerEndpoint string `yaml:"controllerEndpoint,omitempty"` + Airgap bool `yaml:"airgap,omitempty"` } func (agent *LocalAgent) GetName() string { @@ -32,6 +21,12 @@ func (agent *LocalAgent) GetUUID() string { } func (agent *LocalAgent) GetHost() string { + if agent.Config != nil && agent.Config.Host != nil && *agent.Config.Host != "" { + return *agent.Config.Host + } + if agent.Host != "" { + return agent.Host + } return "localhost" } @@ -80,13 +75,20 @@ func (agent *LocalAgent) Clone() Agent { config = new(AgentConfiguration) *config = *agent.Config } + scripts := agent.Scripts + if agent.Scripts != nil { + scripts = new(AgentScripts) + *scripts = *agent.Scripts + } return &LocalAgent{ Name: agent.Name, Host: agent.Host, UUID: agent.UUID, Created: agent.Created, - Container: agent.Container, + Package: agent.Package, + Scripts: scripts, Config: config, ControllerEndpoint: agent.ControllerEndpoint, + Airgap: agent.Airgap, } } diff --git a/internal/resource/local_controller.go b/internal/resource/local_controller.go index c19c6f438..63dd0a9e7 100644 --- a/internal/resource/local_controller.go +++ b/internal/resource/local_controller.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource type LocalController struct { diff --git a/internal/resource/local_controlplane.go b/internal/resource/local_controlplane.go index 81cc7c01c..cebd3f8de 100644 --- a/internal/resource/local_controlplane.go +++ b/internal/resource/local_controlplane.go @@ -1,30 +1,33 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource import ( + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) type LocalControlPlane struct { - IofogUser IofogUser `yaml:"iofogUser"` - Controller *LocalController `yaml:"controller,omitempty"` - Database Database `yaml:"database"` - Auth Auth `yaml:"auth"` - Events Events `yaml:"events,omitempty"` - SystemMicroservices *LocalSystemMicroservices `yaml:"systemMicroservices,omitempty"` - Nats *NatsEnabledConfig `yaml:"nats,omitempty"` + Endpoint string `yaml:"endpoint,omitempty"` + CA string `yaml:"ca,omitempty"` + IofogUser IofogUser `yaml:"iofogUser"` + Controller LocalControllerSpec `yaml:"controller,omitempty"` + Controllers []LocalController `yaml:"controllers,omitempty"` + Database Database `yaml:"database"` + Auth Auth `yaml:"auth"` + RouterSiteCA *SiteCertificate `yaml:"routerSiteCA,omitempty"` + RouterLocalCA *SiteCertificate `yaml:"routerLocalCA,omitempty"` + NatsSiteCA *SiteCertificate `yaml:"natsSiteCA,omitempty"` + NatsLocalCA *SiteCertificate `yaml:"natsLocalCA,omitempty"` + SystemMicroservices install.RemoteSystemMicroservices `yaml:"systemMicroservices,omitempty"` + Nats *NatsEnabledConfig `yaml:"nats,omitempty"` + Events Events `yaml:"events,omitempty"` + Vault *VaultSpec `yaml:"vault,omitempty"` + TLS *ControlPlaneTLS `yaml:"tls,omitempty"` + SystemAgent *SystemAgentConfig `yaml:"systemAgent,omitempty"` + Airgap bool `yaml:"airgap,omitempty"` +} + +func (cp *LocalControlPlane) GetTrustCA() string { + return cp.CA } func (cp *LocalControlPlane) GetUser() IofogUser { @@ -39,24 +42,35 @@ func (cp *LocalControlPlane) UpdateUserTokens(accessToken, refreshToken string) } func (cp *LocalControlPlane) GetControllers() []Controller { - if cp.Controller == nil { - return []Controller{} + controllers := make([]Controller, 0, len(cp.Controllers)) + for idx := range cp.Controllers { + controllers = append(controllers, cp.Controllers[idx].Clone()) } - return []Controller{cp.Controller.Clone()} + return controllers } func (cp *LocalControlPlane) GetController(name string) (Controller, error) { - if cp.Controller == nil { - return nil, util.NewError("Local Control Plane does not have a Controller") + for idx := range cp.Controllers { + if name == "" || cp.Controllers[idx].GetName() == name { + return &cp.Controllers[idx], nil + } } - return cp.Controller, nil + return nil, util.NewError("Local Control Plane does not have a Controller") } func (cp *LocalControlPlane) GetEndpoint() (string, error) { - if cp.Controller == nil { - return "", util.NewError("Local Control Plane does not have a Controller, cannot get endpoint.") + if cp.Endpoint != "" { + return cp.Endpoint, nil + } + if cp.Controller.PublicUrl != "" { + return cp.Controller.PublicUrl, nil } - return cp.Controller.GetEndpoint(), nil + for idx := range cp.Controllers { + if cp.Controllers[idx].Endpoint != "" { + return cp.Controllers[idx].Endpoint, nil + } + } + return "", util.NewError("Local Control Plane does not have an endpoint") } func (cp *LocalControlPlane) UpdateController(baseController Controller) error { @@ -64,7 +78,13 @@ func (cp *LocalControlPlane) UpdateController(baseController Controller) error { if !ok { return util.NewError("Must add Local Controller to Local Control Plane") } - cp.Controller = controller + for idx := range cp.Controllers { + if cp.Controllers[idx].GetName() == controller.GetName() { + cp.Controllers[idx] = *controller + return nil + } + } + cp.Controllers = append(cp.Controllers, *controller) return nil } @@ -73,37 +93,63 @@ func (cp *LocalControlPlane) AddController(baseController Controller) error { if !ok { return util.NewError("Must add Local Controller to Local Control Plane") } - cp.Controller = controller + for idx := range cp.Controllers { + if cp.Controllers[idx].GetName() == controller.GetName() { + return util.NewConflictError(controller.GetName()) + } + } + cp.Controllers = append(cp.Controllers, *controller) return nil } -func (cp *LocalControlPlane) DeleteController(string) error { - cp.Controller = nil - return nil +func (cp *LocalControlPlane) DeleteController(name string) error { + for idx := range cp.Controllers { + if name == "" || cp.Controllers[idx].GetName() == name { + cp.Controllers = append(cp.Controllers[:idx], cp.Controllers[idx+1:]...) + return nil + } + } + return util.NewError("Could not find Controller " + name) } func (cp *LocalControlPlane) Sanitize() error { - if cp.Controller != nil && !util.IsLocalHost(cp.Controller.Endpoint) { - cp.Controller.Endpoint = "localhost" + for idx := range cp.Controllers { + if err := cp.Controllers[idx].Sanitize(); err != nil { + return err + } + if !util.IsLocalHost(cp.Controllers[idx].Endpoint) { + cp.Controllers[idx].Endpoint = "localhost" + } } return nil } func (cp *LocalControlPlane) Clone() ControlPlane { - var sys *LocalSystemMicroservices - if cp.SystemMicroservices != nil { - sys = &LocalSystemMicroservices{ - Router: cp.SystemMicroservices.Router, - Nats: cp.SystemMicroservices.Nats, - } + controllers := make([]LocalController, len(cp.Controllers)) + copy(controllers, cp.Controllers) + var systemAgent *SystemAgentConfig + if cp.SystemAgent != nil { + systemAgent = &SystemAgentConfig{} + *systemAgent = *cp.SystemAgent } return &LocalControlPlane{ + Endpoint: cp.Endpoint, + CA: cp.CA, IofogUser: cp.IofogUser, - Controller: cp.Controller.Clone().(*LocalController), + Controller: cp.Controller, + Controllers: controllers, Database: cp.Database, Auth: cp.Auth, - Events: cp.Events, - SystemMicroservices: sys, + RouterSiteCA: cp.RouterSiteCA, + RouterLocalCA: cp.RouterLocalCA, + NatsSiteCA: cp.NatsSiteCA, + NatsLocalCA: cp.NatsLocalCA, + SystemMicroservices: cp.SystemMicroservices, Nats: cp.Nats, + Events: cp.Events, + Vault: cp.Vault, + TLS: cp.TLS, + SystemAgent: systemAgent, + Airgap: cp.Airgap, } } diff --git a/internal/resource/local_controlplane_golden_test.go b/internal/resource/local_controlplane_golden_test.go new file mode 100644 index 000000000..e08a53f86 --- /dev/null +++ b/internal/resource/local_controlplane_golden_test.go @@ -0,0 +1,181 @@ +package resource + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func localFixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "testdata", "local", name) +} + +func loadLocalFixture(t *testing.T, name string) []byte { + t.Helper() + data, err := os.ReadFile(localFixturePath(name)) + require.NoError(t, err) + return data +} + +func assertGoldenLocalControlPlane(t *testing.T, cp *LocalControlPlane) { + t.Helper() + require.Equal(t, "https://controller.example.com", cp.Endpoint) + require.Equal(t, "Foo", cp.IofogUser.Name) + require.Equal(t, "Bar", cp.IofogUser.Surname) + require.Equal(t, email, cp.IofogUser.Email) + require.Equal(t, "https://controller.example.com", cp.Controller.PublicUrl) + require.Equal(t, "https://controller.example.com", cp.Controller.ConsoleUrl) + require.Equal(t, "info", cp.Controller.LogLevel) + require.NotNil(t, cp.Controller.Package) + require.NotEmpty(t, cp.Controller.Package.Image) + require.Equal(t, "embedded", cp.Auth.Mode) + require.NotNil(t, cp.Auth.Bootstrap) + require.Equal(t, "admin", cp.Auth.Bootstrap.Username) + require.NotNil(t, cp.SystemAgent) + require.NotNil(t, cp.SystemAgent.AgentConfiguration) + require.NotNil(t, cp.SystemAgent.AgentConfiguration.Arch) + require.Equal(t, "amd64", *cp.SystemAgent.AgentConfiguration.Arch) + require.NotNil(t, cp.Nats) + require.NotNil(t, cp.Nats.Enabled) + require.True(t, *cp.Nats.Enabled) + require.NotNil(t, cp.Events.AuditEnabled) + require.True(t, *cp.Events.AuditEnabled) + require.Equal(t, 14, cp.Events.RetentionDays) +} + +func TestGoldenUnmarshalLocalControlPlane_Datasance(t *testing.T) { + raw := loadLocalFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallLocalControlPlane(raw) + require.NoError(t, err) + assertGoldenLocalControlPlane(t, &cp) +} + +func TestGoldenUnmarshalLocalControlPlane_Iofog(t *testing.T) { + raw := loadLocalFixture(t, "controlplane-iofog.yaml") + cp, err := UnmarshallLocalControlPlane(raw) + require.NoError(t, err) + assertGoldenLocalControlPlane(t, &cp) +} + +func TestGoldenUnmarshalLocalControlPlane_WithCA(t *testing.T) { + raw := loadLocalFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallLocalControlPlane(append([]byte("ca: dGVzdC1jYQ==\n"), raw...)) + require.NoError(t, err) + require.Equal(t, "dGVzdC1jYQ==", cp.GetTrustCA()) +} + +func TestGoldenRoundTripLocalControlPlane(t *testing.T) { + raw := loadLocalFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallLocalControlPlane(raw) + require.NoError(t, err) + + out, err := yaml.Marshal(&cp) + require.NoError(t, err) + + var round LocalControlPlane + require.NoError(t, yaml.UnmarshalStrict(out, &round)) + require.Equal(t, cp.Endpoint, round.Endpoint) + require.Equal(t, cp.Controller.PublicUrl, round.Controller.PublicUrl) + require.Equal(t, cp.Auth.Mode, round.Auth.Mode) + require.Equal(t, *cp.SystemAgent.AgentConfiguration.Arch, *round.SystemAgent.AgentConfiguration.Arch) +} + +func requireLocalInputError(t *testing.T, err error) { + t.Helper() + var inputErr *util.InputError + require.ErrorAs(t, err, &inputErr) +} + +func validLocalControlPlane(t *testing.T) *LocalControlPlane { + t.Helper() + raw := loadLocalFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallLocalControlPlane(raw) + require.NoError(t, err) + return &cp +} + +func TestValidateLocalControlPlaneMetadataRejectsControlPlaneType(t *testing.T) { + doc := []byte(`apiVersion: datasance.com/v3 +kind: LocalControlPlane +metadata: + name: local-ecn + controlPlaneType: local +spec: + iofogUser: + email: user@domain.com + auth: + mode: embedded + bootstrap: + username: admin + password: "LocalTest12!" + systemAgent: + config: + arch: amd64 +`) + err := ValidateLocalControlPlaneMetadata(doc) + requireLocalInputError(t, err) +} + +func TestValidateLocalControlPlaneMissingSystemAgent(t *testing.T) { + cp := validLocalControlPlane(t) + cp.SystemAgent = nil + err := ValidateLocalControlPlane(cp) + requireLocalInputError(t, err) +} + +func TestValidateLocalControlPlaneMissingSystemAgentArch(t *testing.T) { + cp := validLocalControlPlane(t) + cp.SystemAgent.AgentConfiguration.Arch = nil + err := ValidateLocalControlPlane(cp) + requireLocalInputError(t, err) +} + +func TestValidateLocalControlPlaneEndpointMismatch(t *testing.T) { + cp := validLocalControlPlane(t) + cp.Controller.PublicUrl = "https://other.example.com" + err := ValidateLocalControlPlane(cp) + requireLocalInputError(t, err) +} + +func TestValidateLocalControlPlanePrivateRegistryIncomplete(t *testing.T) { + cp := validLocalControlPlane(t) + cp.Controller.Package = &ControllerPackage{ + Registry: "ghcr.io", + Username: "foo", + } + err := ValidateLocalControlPlane(cp) + requireLocalInputError(t, err) +} + +func TestValidateLocalControlPlaneEmbeddedAuthWeakPassword(t *testing.T) { + cp := validLocalControlPlane(t) + cp.Auth.Bootstrap.Password = "short" + err := ValidateLocalControlPlane(cp) + requireLocalInputError(t, err) +} + +func TestValidateLocalControlPlaneExternalAuthValid(t *testing.T) { + cp := validLocalControlPlane(t) + cp.Auth = Auth{ + Mode: authModeExternal, + IssuerUrl: "https://auth.example.com/realms/myrealm", + Client: &AuthClient{ + ID: "controller", + Secret: "secret-value", + }, + } + require.NoError(t, ValidateLocalControlPlane(cp)) +} + +func TestValidateLocalControlPlaneDatabaseRequiresFields(t *testing.T) { + cp := validLocalControlPlane(t) + cp.Database = Database{Provider: "postgres"} + err := ValidateLocalControlPlane(cp) + requireLocalInputError(t, err) +} diff --git a/internal/resource/local_controlplane_validate.go b/internal/resource/local_controlplane_validate.go new file mode 100644 index 000000000..910d9cebe --- /dev/null +++ b/internal/resource/local_controlplane_validate.go @@ -0,0 +1,305 @@ +package resource + +import ( + "encoding/base64" + "fmt" + "net/url" + "strings" + + inputvalidate "github.com/eclipse-iofog/iofogctl/internal/validate" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" +) + +const ( + authModeEmbedded = "embedded" + authModeExternal = "external" + localControlPlaneLabel = "Local Control Plane" +) + +var validDatabaseProviders = map[string]struct{}{ + "postgres": {}, + "mysql": {}, +} + +var validVaultProviders = map[string]struct{}{ + "hashicorp": {}, + "openbao": {}, + "vault": {}, + "aws": {}, + "aws-secrets-manager": {}, + "azure": {}, + "azure-key-vault": {}, + "google": {}, + "google-secret-manager": {}, +} + +// ValidateLocalControlPlaneMetadata rejects retired deploy YAML metadata fields. +func ValidateLocalControlPlaneMetadata(fullYAML []byte) error { + var doc struct { + Metadata map[string]interface{} `yaml:"metadata"` + } + if err := yaml.Unmarshal(fullYAML, &doc); err != nil { + return util.NewUnmarshalError(err.Error()) + } + if _, ok := doc.Metadata["controlPlaneType"]; ok { + return util.NewInputError("metadata.controlPlaneType is retired; use kind: LocalControlPlane") + } + return nil +} + +// ValidateLocalControlPlane validates a parsed LocalControlPlane spec. +func ValidateLocalControlPlane(cp *LocalControlPlane) error { + if err := validateIofogUser(localControlPlaneLabel, cp.IofogUser); err != nil { + return err + } + if err := validateAuth(localControlPlaneLabel, cp.Auth); err != nil { + return err + } + if err := validateLocalSystemAgent(cp.SystemAgent); err != nil { + return err + } + if err := validateEndpointMatch(localControlPlaneLabel, cp.Endpoint, cp.Controller.PublicUrl); err != nil { + return err + } + if err := validateControllerPackage(localControlPlaneLabel, cp.Controller.Package); err != nil { + return err + } + if err := validateDatabase(localControlPlaneLabel, cp.Database); err != nil { + return err + } + if err := validateLocalSystemMicroservices(cp.SystemMicroservices); err != nil { + return err + } + if err := validateCAField(localControlPlaneLabel, "ca", cp.CA); err != nil { + return err + } + if err := validateSiteCertificateBlock(localControlPlaneLabel, "routerSiteCA", cp.RouterSiteCA); err != nil { + return err + } + if err := validateSiteCertificateBlock(localControlPlaneLabel, "routerLocalCA", cp.RouterLocalCA); err != nil { + return err + } + if err := validateSiteCertificateBlock(localControlPlaneLabel, "natsSiteCA", cp.NatsSiteCA); err != nil { + return err + } + if err := validateSiteCertificateBlock(localControlPlaneLabel, "natsLocalCA", cp.NatsLocalCA); err != nil { + return err + } + if err := validateControlPlaneTLS(localControlPlaneLabel, cp.TLS); err != nil { + return err + } + if err := validateVault(localControlPlaneLabel, cp.Vault); err != nil { + return err + } + return nil +} + +func validateIofogUser(label string, user IofogUser) error { + if user.Email == "" { + return util.NewInputError(label + " iofogUser.email is required") + } + if rawPassword := user.GetRawPassword(); rawPassword != "" { + if err := inputvalidate.ValidatePasswordComplexity(rawPassword); err != nil { + return err + } + } + return nil +} + +func validateAuth(label string, auth Auth) error { + switch auth.Mode { + case authModeEmbedded: + return validateEmbeddedAuth(label, auth) + case authModeExternal: + return validateExternalAuth(label, auth) + case "": + return util.NewInputError(label + " auth.mode is required (embedded or external)") + default: + return util.NewInputError(fmt.Sprintf("%s auth.mode %q is invalid (embedded or external)", label, auth.Mode)) + } +} + +func validateEmbeddedAuth(label string, auth Auth) error { + if auth.Bootstrap == nil { + return util.NewInputError(label + " auth.bootstrap is required when auth.mode is embedded") + } + if auth.Bootstrap.Username == "" { + return util.NewInputError(label + " auth.bootstrap.username is required when auth.mode is embedded") + } + if auth.Bootstrap.Password == "" { + return util.NewInputError(label + " auth.bootstrap.password is required in YAML when auth.mode is embedded") + } + return inputvalidate.ValidatePasswordComplexity(auth.Bootstrap.Password) +} + +func validateExternalAuth(label string, auth Auth) error { + if auth.IssuerUrl == "" { + return util.NewInputError(label + " auth.issuerUrl is required when auth.mode is external") + } + if auth.Client == nil || auth.Client.ID == "" { + return util.NewInputError(label + " auth.client.id is required when auth.mode is external") + } + if auth.Client.Secret == "" { + return util.NewInputError(label + " auth.client.secret is required when auth.mode is external") + } + return nil +} + +func validateLocalSystemAgent(systemAgent *SystemAgentConfig) error { + if systemAgent == nil { + return util.NewInputError("Local Control Plane systemAgent is required") + } + if systemAgent.AgentConfiguration == nil || systemAgent.AgentConfiguration.Arch == nil || *systemAgent.AgentConfiguration.Arch == "" { + return util.NewInputError("Local Control Plane systemAgent.config.arch is required") + } + if _, ok := ArchStringToID(*systemAgent.AgentConfiguration.Arch); !ok { + return util.NewInputError(fmt.Sprintf("Local Control Plane systemAgent.config.arch %q is invalid", *systemAgent.AgentConfiguration.Arch)) + } + return validateSystemAgentRouterNats(localControlPlaneLabel, systemAgent.AgentConfiguration) +} + +func validateEndpointMatch(label, endpoint, publicURL string) error { + if endpoint != "" && publicURL != "" && endpoint != publicURL { + return util.NewInputError(label + " spec.endpoint must match spec.controller.publicUrl when both are set") + } + for _, value := range []string{endpoint, publicURL} { + if value == "" { + continue + } + if err := validateOptionalURL(value); err != nil { + return util.NewInputError(fmt.Sprintf("%s endpoint URL %q is invalid: %v", label, value, err)) + } + } + return nil +} + +func validateOptionalURL(raw string) error { + parsed, err := url.Parse(raw) + if err != nil { + return err + } + if parsed.Host == "" { + return fmt.Errorf("missing host") + } + return nil +} + +func validateControllerPackage(label string, pkg *ControllerPackage) error { + if pkg == nil { + return nil + } + hasRegistry := pkg.Registry != "" + hasUsername := pkg.Username != "" + hasPassword := pkg.Password != "" + if hasRegistry || hasUsername || hasPassword { + if !hasRegistry || !hasUsername || !hasPassword { + return util.NewInputError(label + " controller.package requires registry, username, and password for private registry access") + } + } + return nil +} + +func validateDatabase(label string, db Database) error { + if db.Provider == "" { + return nil + } + if _, ok := validDatabaseProviders[db.Provider]; !ok { + return util.NewInputError(fmt.Sprintf("%s database.provider %q is invalid (postgres or mysql)", label, db.Provider)) + } + if db.User == "" || db.Host == "" || db.DatabaseName == "" || db.Password == "" || db.Port == 0 { + return util.NewInputError(label + " database requires user, host, port, password, and databaseName when provider is set") + } + return nil +} + +func validateLocalSystemMicroservices(sys install.RemoteSystemMicroservices) error { + return nil +} + +func validateCAField(label, name, value string) error { + if value == "" { + return nil + } + if _, err := base64.StdEncoding.DecodeString(value); err != nil { + return util.NewInputError(fmt.Sprintf("%s %s must be valid base64", label, name)) + } + return nil +} + +func validateSiteCertificateBlock(label, name string, cert *SiteCertificate) error { + if cert == nil { + return nil + } + if cert.TLSCert != "" { + if _, err := base64.StdEncoding.DecodeString(cert.TLSCert); err != nil { + return util.NewInputError(fmt.Sprintf("%s %s.tlsCert must be valid base64", label, name)) + } + } + if cert.TLSKey != "" { + if _, err := base64.StdEncoding.DecodeString(cert.TLSKey); err != nil { + return util.NewInputError(fmt.Sprintf("%s %s.tlsKey must be valid base64", label, name)) + } + } + return nil +} + +func validateControlPlaneTLS(label string, tls *ControlPlaneTLS) error { + if tls == nil { + return nil + } + for _, value := range []struct { + name string + raw string + }{ + {"tls.ca", tls.CA}, + {"tls.cert", tls.Cert}, + {"tls.key", tls.Key}, + } { + if value.raw == "" { + continue + } + if _, err := base64.StdEncoding.DecodeString(value.raw); err != nil { + return util.NewInputError(fmt.Sprintf("%s %s must be valid base64", label, value.name)) + } + } + if (tls.Cert == "") != (tls.Key == "") { + return util.NewInputError(label + " tls.cert and tls.key must both be set when either is provided") + } + return nil +} + +func validateVault(label string, vault *VaultSpec) error { + if vault == nil || vault.Enabled == nil || !*vault.Enabled { + return nil + } + if vault.Provider == "" { + return util.NewInputError(label + " vault.provider is required when vault.enabled is true") + } + if vault.BasePath == "" { + return util.NewInputError(label + " vault.basePath is required when vault.enabled is true") + } + if _, ok := validVaultProviders[strings.ToLower(vault.Provider)]; !ok { + return util.NewInputError(fmt.Sprintf("%s vault.provider %q is invalid", label, vault.Provider)) + } + switch strings.ToLower(vault.Provider) { + case "hashicorp", "openbao", "vault": + if vault.Hashicorp == nil || vault.Hashicorp.Address == "" || vault.Hashicorp.Token == "" { + return util.NewInputError(label + " vault.hashicorp is required for the selected vault provider") + } + case "aws", "aws-secrets-manager": + if vault.Aws == nil || vault.Aws.Region == "" { + return util.NewInputError(label + " vault.aws is required for the selected vault provider") + } + case "azure", "azure-key-vault": + if vault.Azure == nil || vault.Azure.URL == "" { + return util.NewInputError(label + " vault.azure is required for the selected vault provider") + } + case "google", "google-secret-manager": + if vault.Google == nil || vault.Google.ProjectId == "" { + return util.NewInputError(label + " vault.google is required for the selected vault provider") + } + } + return nil +} diff --git a/internal/resource/namespace_test.go b/internal/resource/namespace_test.go index a09ae825f..a2f439d7c 100644 --- a/internal/resource/namespace_test.go +++ b/internal/resource/namespace_test.go @@ -23,7 +23,7 @@ func TestAgents(t *testing.T) { // Add for idx := range agents { if err := ns.AddAgent(agents[idx]); err != nil { - t.Errorf("Failed to create Agent: " + err.Error()) + t.Errorf("Failed to create Agent: %s", err.Error()) } } if len(ns.GetAgents()) != 2 { @@ -46,7 +46,7 @@ func TestAgents(t *testing.T) { for idx := 0; idx < len(agents)*2; idx++ { modIdx := idx % len(agents) if err := ns.UpdateAgent(agents[modIdx]); err != nil { - t.Errorf("Failed to update Agent: " + err.Error()) + t.Errorf("Failed to update Agent: %s", err.Error()) } } if len(ns.GetAgents()) != 2 { diff --git a/internal/resource/remote_agent.go b/internal/resource/remote_agent.go index 10e95e684..e522f8089 100644 --- a/internal/resource/remote_agent.go +++ b/internal/resource/remote_agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource import ( diff --git a/internal/resource/remote_controller.go b/internal/resource/remote_controller.go index 5c3adaab0..2acffef8f 100644 --- a/internal/resource/remote_controller.go +++ b/internal/resource/remote_controller.go @@ -1,28 +1,9 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource import ( - "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) -type ControllerScripts struct { - install.ControllerProcedures `yaml:",inline"` - Directory string `yaml:"dir"` // Location of scripts -} - type SystemAgentConfig struct { Package Package `yaml:"package,omitempty"` Scripts *AgentScripts `yaml:"scripts,omitempty"` // Custom scripts @@ -30,15 +11,13 @@ type SystemAgentConfig struct { } type RemoteController struct { - RemoteControllerConfig `yaml:",inline"` - Name string `yaml:"name"` - Host string `yaml:"host"` - SSH SSH `yaml:"ssh,omitempty"` - Endpoint string `yaml:"endpoint,omitempty"` - Created string `yaml:"created,omitempty"` - Scripts *ControllerScripts `yaml:"scripts,omitempty"` - SystemAgent *SystemAgentConfig `yaml:"systemAgent,omitempty"` // Per-controller system agent config - Airgap bool `yaml:"airgap,omitempty"` + Name string `yaml:"name"` + Host string `yaml:"host"` + SSH SSH `yaml:"ssh,omitempty"` + TLS *ControlPlaneTLS `yaml:"tls,omitempty"` + SystemAgent *SystemAgentConfig `yaml:"systemAgent,omitempty"` + Endpoint string `yaml:"endpoint,omitempty"` + Created string `yaml:"created,omitempty"` } func (ctrl *RemoteController) GetName() string { @@ -58,11 +37,9 @@ func (ctrl *RemoteController) SetName(name string) { } func (ctrl *RemoteController) Sanitize() (err error) { - // Fix SSH port if ctrl.Host != "" && ctrl.SSH.Port == 0 { ctrl.SSH.Port = 22 } - // Format file paths if ctrl.SSH.KeyFile, err = util.FormatPath(ctrl.SSH.KeyFile); err != nil { return } @@ -70,26 +47,19 @@ func (ctrl *RemoteController) Sanitize() (err error) { } func (ctrl *RemoteController) Clone() Controller { - scripts := ctrl.Scripts - if ctrl.Scripts != nil { - scripts = new(ControllerScripts) - *scripts = *ctrl.Scripts - } - systemAgent := ctrl.SystemAgent + var systemAgent *SystemAgentConfig if ctrl.SystemAgent != nil { systemAgent = new(SystemAgentConfig) *systemAgent = *ctrl.SystemAgent } return &RemoteController{ - RemoteControllerConfig: ctrl.RemoteControllerConfig, - Name: ctrl.Name, - Host: ctrl.Host, - SSH: ctrl.SSH, - Endpoint: ctrl.Endpoint, - Created: ctrl.Created, - Scripts: scripts, - SystemAgent: systemAgent, - Airgap: ctrl.Airgap, + Name: ctrl.Name, + Host: ctrl.Host, + SSH: ctrl.SSH, + TLS: ctrl.TLS, + SystemAgent: systemAgent, + Endpoint: ctrl.Endpoint, + Created: ctrl.Created, } } diff --git a/internal/resource/remote_controlplane.go b/internal/resource/remote_controlplane.go index 0dd495050..22897c7e0 100644 --- a/internal/resource/remote_controlplane.go +++ b/internal/resource/remote_controlplane.go @@ -1,19 +1,8 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource import ( + "fmt" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -21,19 +10,29 @@ import ( type RemoteSystemMicroservices = install.RemoteSystemMicroservices type RemoteControlPlane struct { + Endpoint string `yaml:"endpoint,omitempty"` + CA string `yaml:"ca,omitempty"` IofogUser IofogUser `yaml:"iofogUser"` + Controller LocalControllerSpec `yaml:"controller,omitempty"` Controllers []RemoteController `yaml:"controllers"` Database Database `yaml:"database"` Auth Auth `yaml:"auth"` - Events Events `yaml:"events,omitempty"` - Package Package `yaml:"package,omitempty"` + RouterSiteCA *SiteCertificate `yaml:"routerSiteCA,omitempty"` + RouterLocalCA *SiteCertificate `yaml:"routerLocalCA,omitempty"` + NatsSiteCA *SiteCertificate `yaml:"natsSiteCA,omitempty"` + NatsLocalCA *SiteCertificate `yaml:"natsLocalCA,omitempty"` SystemMicroservices RemoteSystemMicroservices `yaml:"systemMicroservices,omitempty"` Nats *NatsEnabledConfig `yaml:"nats,omitempty"` + Events Events `yaml:"events,omitempty"` Vault *VaultSpec `yaml:"vault,omitempty"` - Endpoint string `yaml:"endpoint,omitempty"` + TLS *ControlPlaneTLS `yaml:"tls,omitempty"` Airgap bool `yaml:"airgap,omitempty"` } +func (cp *RemoteControlPlane) GetTrustCA() string { + return cp.CA +} + func (cp *RemoteControlPlane) GetUser() IofogUser { return cp.IofogUser } @@ -64,12 +63,12 @@ func (cp *RemoteControlPlane) GetController(name string) (ret Controller, err er } func (cp *RemoteControlPlane) GetEndpoint() (string, error) { - // 1. Check if external endpoint (load balancer) is configured if cp.Endpoint != "" { return cp.Endpoint, nil } - - // 2. Fall back to existing logic (first controller with endpoint) + if cp.Controller.PublicUrl != "" { + return cp.Controller.PublicUrl, nil + } if len(cp.Controllers) == 0 { return "", util.NewInternalError("Control Plane does not have any Controllers") } @@ -128,20 +127,71 @@ func (cp *RemoteControlPlane) Sanitize() (err error) { return nil } +const controllerAddOnConnectHint = "use connect -f with a full controlplane.yaml or deploy -f controlplane.yaml first" + +// SupportsControllerAddOn reports whether the stored Control Plane has enough deploy +// metadata to add controllers via standalone kind: Controller YAML. +func (cp *RemoteControlPlane) SupportsControllerAddOn() error { + if cp.Auth.Mode == "" { + return util.NewInputError("namespace Control Plane does not support adding controllers; " + controllerAddOnConnectHint) + } + if !hasControllerSpec(cp.Controller) { + return util.NewInputError("namespace Control Plane is missing spec.controller; " + controllerAddOnConnectHint) + } + return nil +} + +// ValidateControllerAddOn rejects duplicate controller name or host in the stored CP. +func (cp *RemoteControlPlane) ValidateControllerAddOn(ctrl *RemoteController) error { + for _, existing := range cp.Controllers { + if existing.Name == ctrl.Name { + return util.NewInputError(fmt.Sprintf("controller name %q already exists in namespace Control Plane", ctrl.Name)) + } + if existing.Host != "" && existing.Host == ctrl.Host { + return util.NewInputError(fmt.Sprintf("controller host %q already exists in namespace Control Plane", ctrl.Host)) + } + } + return nil +} + +// ValidateControllerAddOnDatabase rejects SQLite when adding a second controller. +func (cp *RemoteControlPlane) ValidateControllerAddOnDatabase() error { + if len(cp.Controllers) >= 1 && cp.Database.Provider == "" { + return util.NewInputError("cannot add controller: external database is required when multiple controllers are configured") + } + return nil +} + +func hasControllerSpec(c LocalControllerSpec) bool { + if c.PublicUrl != "" || c.ConsoleUrl != "" || c.LogLevel != "" || c.PidBaseDir != "" || c.Package != nil { + return true + } + if c.ConsolePort != 0 || c.TrustProxy != nil || c.Https != nil || c.SecretName != "" { + return true + } + return false +} + func (cp *RemoteControlPlane) Clone() ControlPlane { controllers := make([]RemoteController, len(cp.Controllers)) copy(controllers, cp.Controllers) return &RemoteControlPlane{ + Endpoint: cp.Endpoint, + CA: cp.CA, IofogUser: cp.IofogUser, + Controller: cp.Controller, + Controllers: controllers, Database: cp.Database, Auth: cp.Auth, - Events: cp.Events, - Package: cp.Package, + RouterSiteCA: cp.RouterSiteCA, + RouterLocalCA: cp.RouterLocalCA, + NatsSiteCA: cp.NatsSiteCA, + NatsLocalCA: cp.NatsLocalCA, SystemMicroservices: cp.SystemMicroservices, Nats: cp.Nats, + Events: cp.Events, Vault: cp.Vault, - Controllers: controllers, - Endpoint: cp.Endpoint, + TLS: cp.TLS, Airgap: cp.Airgap, } } diff --git a/internal/resource/remote_controlplane_addon_test.go b/internal/resource/remote_controlplane_addon_test.go new file mode 100644 index 000000000..995344895 --- /dev/null +++ b/internal/resource/remote_controlplane_addon_test.go @@ -0,0 +1,75 @@ +package resource + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func fullRemoteControlPlane() *RemoteControlPlane { + return &RemoteControlPlane{ + Auth: Auth{Mode: "embedded"}, + Controller: LocalControllerSpec{ + ControllerConfig: ControllerConfig{PublicUrl: "http://192.168.1.6:51121"}, + }, + Controllers: []RemoteController{{Name: "remote-1", Host: "10.0.0.1"}}, + } +} + +func TestSupportsControllerAddOnThinCPRejected(t *testing.T) { + cp := &RemoteControlPlane{ + IofogUser: IofogUser{Email: "user@example.com"}, + Controllers: []RemoteController{{Name: "ctrl-1", Host: "10.0.0.1", Endpoint: "http://10.0.0.1:51121"}}, + } + err := cp.SupportsControllerAddOn() + require.Error(t, err) + require.Contains(t, err.Error(), "connect -f") +} + +func TestSupportsControllerAddOnMissingControllerBlock(t *testing.T) { + cp := &RemoteControlPlane{ + Auth: Auth{Mode: "embedded"}, + Controllers: []RemoteController{{Name: "remote-1", Host: "10.0.0.1"}}, + } + err := cp.SupportsControllerAddOn() + require.Error(t, err) + require.Contains(t, err.Error(), "spec.controller") +} + +func TestSupportsControllerAddOnFullCPOK(t *testing.T) { + cp := fullRemoteControlPlane() + require.NoError(t, cp.SupportsControllerAddOn()) +} + +func TestValidateControllerAddOnDatabaseSQLiteRejected(t *testing.T) { + cp := fullRemoteControlPlane() + err := cp.ValidateControllerAddOnDatabase() + require.Error(t, err) + require.Contains(t, err.Error(), "external database") +} + +func TestValidateControllerAddOnDatabaseExternalOK(t *testing.T) { + cp := fullRemoteControlPlane() + cp.Database = Database{Provider: "postgres", Host: "db.example.com"} + require.NoError(t, cp.ValidateControllerAddOnDatabase()) +} + +func TestValidateControllerAddOnDuplicateName(t *testing.T) { + cp := fullRemoteControlPlane() + err := cp.ValidateControllerAddOn(&RemoteController{Name: "remote-1", Host: "10.0.0.2"}) + require.Error(t, err) + require.Contains(t, err.Error(), "name") +} + +func TestValidateControllerAddOnDuplicateHost(t *testing.T) { + cp := fullRemoteControlPlane() + err := cp.ValidateControllerAddOn(&RemoteController{Name: "remote-2", Host: "10.0.0.1"}) + require.Error(t, err) + require.Contains(t, err.Error(), "host") +} + +func TestValidateControllerAddOnUniqueOK(t *testing.T) { + cp := fullRemoteControlPlane() + err := cp.ValidateControllerAddOn(&RemoteController{Name: "remote-2", Host: "10.0.0.2"}) + require.NoError(t, err) +} diff --git a/internal/resource/remote_controlplane_golden_test.go b/internal/resource/remote_controlplane_golden_test.go new file mode 100644 index 000000000..8a245d2b6 --- /dev/null +++ b/internal/resource/remote_controlplane_golden_test.go @@ -0,0 +1,190 @@ +package resource + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func remoteFixturePath(name string) string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "testdata", "remote", name) +} + +func loadRemoteFixture(t *testing.T, name string) []byte { + t.Helper() + data, err := os.ReadFile(remoteFixturePath(name)) + require.NoError(t, err) + return data +} + +func assertGoldenRemoteControlPlane(t *testing.T, cp *RemoteControlPlane) { + t.Helper() + require.Equal(t, "https://controller.example.com", cp.Endpoint) + require.Equal(t, "Foo", cp.IofogUser.Name) + require.Equal(t, "Bar", cp.IofogUser.Surname) + require.Equal(t, email, cp.IofogUser.Email) + require.Equal(t, "https://controller.example.com", cp.Controller.PublicUrl) + require.Equal(t, "https://controller.example.com", cp.Controller.ConsoleUrl) + require.Equal(t, "info", cp.Controller.LogLevel) + require.NotNil(t, cp.Controller.Package) + require.NotEmpty(t, cp.Controller.Package.Image) + require.Equal(t, "embedded", cp.Auth.Mode) + require.NotNil(t, cp.Auth.Bootstrap) + require.Equal(t, "admin", cp.Auth.Bootstrap.Username) + require.Len(t, cp.Controllers, 2) + require.Equal(t, "remote-1", cp.Controllers[0].Name) + require.Equal(t, "10.0.128.192", cp.Controllers[0].Host) + require.NotNil(t, cp.Controllers[0].SystemAgent) + require.Equal(t, "foo", cp.Controllers[0].SSH.User) + require.Equal(t, "remote-2", cp.Controllers[1].Name) + require.NotNil(t, cp.Controllers[1].SystemAgent) + require.NotNil(t, cp.Nats) + require.NotNil(t, cp.Nats.Enabled) + require.True(t, *cp.Nats.Enabled) + require.NotNil(t, cp.Events.AuditEnabled) + require.True(t, *cp.Events.AuditEnabled) + require.Equal(t, 14, cp.Events.RetentionDays) +} + +func TestGoldenUnmarshalRemoteControlPlane_Datasance(t *testing.T) { + raw := loadRemoteFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallRemoteControlPlane(raw) + require.NoError(t, err) + assertGoldenRemoteControlPlane(t, &cp) +} + +func TestGoldenUnmarshalRemoteControlPlane_Iofog(t *testing.T) { + raw := loadRemoteFixture(t, "controlplane-iofog.yaml") + cp, err := UnmarshallRemoteControlPlane(raw) + require.NoError(t, err) + assertGoldenRemoteControlPlane(t, &cp) +} + +func TestGoldenUnmarshalRemoteControlPlane_WithCA(t *testing.T) { + raw := loadRemoteFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallRemoteControlPlane(append([]byte("ca: dGVzdC1jYQ==\n"), raw...)) + require.NoError(t, err) + require.Equal(t, "dGVzdC1jYQ==", cp.GetTrustCA()) +} + +func TestGoldenRoundTripRemoteControlPlane(t *testing.T) { + raw := loadRemoteFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallRemoteControlPlane(raw) + require.NoError(t, err) + + out, err := yaml.Marshal(&cp) + require.NoError(t, err) + + var round RemoteControlPlane + require.NoError(t, yaml.UnmarshalStrict(out, &round)) + require.Equal(t, cp.Endpoint, round.Endpoint) + require.Equal(t, cp.Controller.PublicUrl, round.Controller.PublicUrl) + require.Equal(t, cp.Auth.Mode, round.Auth.Mode) + require.Len(t, round.Controllers, len(cp.Controllers)) +} + +func requireRemoteInputError(t *testing.T, err error) { + t.Helper() + var inputErr *util.InputError + require.ErrorAs(t, err, &inputErr) +} + +func validRemoteControlPlane(t *testing.T) *RemoteControlPlane { + t.Helper() + raw := loadRemoteFixture(t, "controlplane-datasance.yaml") + cp, err := UnmarshallRemoteControlPlane(raw) + require.NoError(t, err) + return &cp +} + +func TestValidateRemoteControlPlaneMetadataRejectsControlPlaneType(t *testing.T) { + doc := []byte(`apiVersion: datasance.com/v3 +kind: ControlPlane +metadata: + name: remote-ecn + controlPlaneType: remote +spec: + iofogUser: + email: user@domain.com + auth: + mode: embedded + bootstrap: + username: admin + password: "RemoteTest12!" + controllers: + - name: remote-1 + host: 10.0.0.1 + ssh: + user: foo + keyFile: ~/.ssh/id_rsa + systemAgent: {} +`) + err := ValidateRemoteControlPlaneMetadata(doc) + requireRemoteInputError(t, err) +} + +func TestValidateRemoteControlPlaneSingleControllerSQLiteOK(t *testing.T) { + raw := loadRemoteFixture(t, "controlplane-single.yaml") + cp, err := UnmarshallRemoteControlPlane(raw) + require.NoError(t, err) + require.Empty(t, cp.Database.Provider) + require.Len(t, cp.Controllers, 1) +} + +func TestValidateRemoteControlPlaneMultiControllerRequiresDatabase(t *testing.T) { + cp := validRemoteControlPlane(t) + cp.Database = Database{} + err := ValidateRemoteControlPlane(cp) + requireRemoteInputError(t, err) +} + +func TestValidateRemoteControlPlaneMissingSystemAgent(t *testing.T) { + cp := validRemoteControlPlane(t) + cp.Controllers[0].SystemAgent = nil + err := ValidateRemoteControlPlane(cp) + requireRemoteInputError(t, err) +} + +func TestValidateRemoteControlPlaneSystemAgentArchWhenConfigSet(t *testing.T) { + cp := validRemoteControlPlane(t) + cp.Controllers[0].SystemAgent.AgentConfiguration = &AgentConfiguration{} + err := ValidateRemoteControlPlane(cp) + requireRemoteInputError(t, err) +} + +func TestValidateRemoteControlPlaneEndpointMismatch(t *testing.T) { + cp := validRemoteControlPlane(t) + cp.Controller.PublicUrl = "https://other.example.com" + err := ValidateRemoteControlPlane(cp) + requireRemoteInputError(t, err) +} + +func TestValidateRemoteControlPlanePrivateRegistryIncomplete(t *testing.T) { + cp := validRemoteControlPlane(t) + cp.Controller.Package = &ControllerPackage{ + Registry: "ghcr.io", + Username: "foo", + } + err := ValidateRemoteControlPlane(cp) + requireRemoteInputError(t, err) +} + +func TestValidateRemoteControlPlaneDuplicateControllerName(t *testing.T) { + cp := validRemoteControlPlane(t) + cp.Controllers[1].Name = cp.Controllers[0].Name + err := ValidateRemoteControlPlane(cp) + requireRemoteInputError(t, err) +} + +func TestValidateRemoteControlPlaneAirgapRequiresArch(t *testing.T) { + cp := validRemoteControlPlane(t) + cp.Airgap = true + err := ValidateRemoteControlPlane(cp) + requireRemoteInputError(t, err) +} diff --git a/internal/resource/remote_controlplane_validate.go b/internal/resource/remote_controlplane_validate.go new file mode 100644 index 000000000..9b07695b9 --- /dev/null +++ b/internal/resource/remote_controlplane_validate.go @@ -0,0 +1,153 @@ +package resource + +import ( + "fmt" + "strings" + + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" +) + +const remoteControlPlaneLabel = "Remote Control Plane" + +// ValidateRemoteControlPlaneMetadata rejects retired deploy YAML metadata fields. +func ValidateRemoteControlPlaneMetadata(fullYAML []byte) error { + var doc struct { + Metadata map[string]interface{} `yaml:"metadata"` + } + if err := yaml.Unmarshal(fullYAML, &doc); err != nil { + return util.NewUnmarshalError(err.Error()) + } + if _, ok := doc.Metadata["controlPlaneType"]; ok { + return util.NewInputError("metadata.controlPlaneType is retired; use kind: ControlPlane") + } + return nil +} + +// ValidateRemoteControlPlane validates a parsed RemoteControlPlane spec. +func ValidateRemoteControlPlane(cp *RemoteControlPlane) error { + if err := validateIofogUser(remoteControlPlaneLabel, cp.IofogUser); err != nil { + return err + } + if err := validateAuth(remoteControlPlaneLabel, cp.Auth); err != nil { + return err + } + if err := validateEndpointMatch(remoteControlPlaneLabel, cp.Endpoint, cp.Controller.PublicUrl); err != nil { + return err + } + if err := validateControllerPackage(remoteControlPlaneLabel, cp.Controller.Package); err != nil { + return err + } + if err := validateRemoteDatabase(cp.Database, len(cp.Controllers)); err != nil { + return err + } + if err := validateRemoteControllers(cp.Controllers); err != nil { + return err + } + if cp.Airgap { + if err := validateRemoteControlPlaneAirgapArch(cp.Controllers); err != nil { + return err + } + } + if err := validateRemoteSystemMicroservices(cp.SystemMicroservices); err != nil { + return err + } + if err := validateCAField(remoteControlPlaneLabel, "ca", cp.CA); err != nil { + return err + } + if err := validateSiteCertificateBlock(remoteControlPlaneLabel, "routerSiteCA", cp.RouterSiteCA); err != nil { + return err + } + if err := validateSiteCertificateBlock(remoteControlPlaneLabel, "routerLocalCA", cp.RouterLocalCA); err != nil { + return err + } + if err := validateSiteCertificateBlock(remoteControlPlaneLabel, "natsSiteCA", cp.NatsSiteCA); err != nil { + return err + } + if err := validateSiteCertificateBlock(remoteControlPlaneLabel, "natsLocalCA", cp.NatsLocalCA); err != nil { + return err + } + if err := validateControlPlaneTLS(remoteControlPlaneLabel, cp.TLS); err != nil { + return err + } + if err := validateVault(remoteControlPlaneLabel, cp.Vault); err != nil { + return err + } + return nil +} + +func validateRemoteDatabase(db Database, controllerCount int) error { + if controllerCount > 1 { + if db.Provider == "" { + return util.NewInputError("Remote Control Plane database is required when multiple controllers are configured") + } + } + if db.Provider == "" { + return nil + } + return validateDatabase(remoteControlPlaneLabel, db) +} + +func validateRemoteControllers(controllers []RemoteController) error { + if len(controllers) == 0 { + return util.NewInputError("Remote Control Plane requires at least one controller") + } + seen := make(map[string]struct{}, len(controllers)) + for _, ctrl := range controllers { + if err := util.IsLowerAlphanumeric("Controller", ctrl.Name); err != nil { + return err + } + if _, exists := seen[ctrl.Name]; exists { + return util.NewInputError(fmt.Sprintf("Remote Control Plane controller name %q must be unique", ctrl.Name)) + } + seen[ctrl.Name] = struct{}{} + if ctrl.Host == "" || ctrl.SSH.User == "" || ctrl.SSH.KeyFile == "" { + return util.NewInputError(fmt.Sprintf("Remote Control Plane controller %q requires host, ssh.user, and ssh.keyFile", ctrl.Name)) + } + if ctrl.SystemAgent == nil { + return util.NewInputError(fmt.Sprintf("Remote Control Plane controller %q requires systemAgent", ctrl.Name)) + } + if err := validateRemoteControllerSystemAgent(ctrl.Name, ctrl.SystemAgent); err != nil { + return err + } + if err := validateControlPlaneTLS(remoteControlPlaneLabel, ctrl.TLS); err != nil { + return err + } + } + return nil +} + +func validateRemoteControllerSystemAgent(controllerName string, systemAgent *SystemAgentConfig) error { + if systemAgent.AgentConfiguration == nil { + return nil + } + cfg := systemAgent.AgentConfiguration + if cfg.Arch == nil || *cfg.Arch == "" { + return util.NewInputError(fmt.Sprintf("Remote Control Plane controller %q systemAgent.config.arch is required when systemAgent.config is set", controllerName)) + } + if _, ok := ArchStringToID(*cfg.Arch); !ok { + return util.NewInputError(fmt.Sprintf("Remote Control Plane controller %q systemAgent.config.arch %q is invalid", controllerName, *cfg.Arch)) + } + return validateSystemAgentRouterNats(fmt.Sprintf("Remote Control Plane controller %q", controllerName), cfg) +} + +func validateRemoteControlPlaneAirgapArch(controllers []RemoteController) error { + for _, ctrl := range controllers { + if ctrl.SystemAgent == nil || ctrl.SystemAgent.AgentConfiguration == nil { + return util.NewInputError(fmt.Sprintf("Remote Control Plane controller %q requires systemAgent.config.arch when airgap is enabled", ctrl.Name)) + } + arch := ctrl.SystemAgent.AgentConfiguration.Arch + if arch == nil || strings.TrimSpace(*arch) == "" { + return util.NewInputError(fmt.Sprintf("Remote Control Plane controller %q requires systemAgent.config.arch when airgap is enabled", ctrl.Name)) + } + if _, ok := ArchStringToID(*arch); !ok { + return util.NewInputError(fmt.Sprintf("Remote Control Plane controller %q systemAgent.config.arch %q is invalid", ctrl.Name, *arch)) + } + } + return nil +} + +func validateRemoteSystemMicroservices(_ install.RemoteSystemMicroservices) error { + return nil +} diff --git a/internal/resource/testdata/edgelet/agent-config-spec.yaml b/internal/resource/testdata/edgelet/agent-config-spec.yaml new file mode 100644 index 000000000..434b3c7dd --- /dev/null +++ b/internal/resource/testdata/edgelet/agent-config-spec.yaml @@ -0,0 +1,42 @@ +description: agent running on VM +latitude: 46.204391 +longitude: 6.143158 +arch: riscv64 +containerEngine: edgelet +containerEngineUrl: unix:///run/edgelet/containerd.sock +diskLimit: 50 +diskDirectory: /var/lib/edgelet/ +memoryLimit: 4096 +cpuLimit: 80 +logLimit: 10 +logDirectory: /var/log/edgelet/ +logFileCount: 10 +statusFrequency: 10 +changeFrequency: 10 +deviceScanFrequency: 60 +bluetoothEnabled: true +watchdogEnabled: false +gpsMode: auto +gpsScanFrequency: 60 +gpsDevice: '' +edgeGuardFrequency: 0 +abstractedHardwareEnabled: false +upstreamRouters: ['default-router'] +routerConfig: + routerMode: edge + messagingPort: 5671 + edgeRouterPort: 45671 + interRouterPort: 55671 +upstreamNatsServers: + - default-nats-hub +natsConfig: + natsMode: leaf + natsServerPort: 4222 + natsLeafPort: 7422 + natsMqttPort: 8883 + natsHttpPort: 8222 + jsStorageSize: 10g + jsMemoryStoreSize: 1g +pruningFrequency: 0 +logLevel: INFO +availableDiskThreshold: 90 diff --git a/internal/resource/testdata/edgelet/local-agent-spec.yaml b/internal/resource/testdata/edgelet/local-agent-spec.yaml new file mode 100644 index 000000000..3f80c1542 --- /dev/null +++ b/internal/resource/testdata/edgelet/local-agent-spec.yaml @@ -0,0 +1,49 @@ +package: + version: 1.0.0-rc.6 + container: + image: ghcr.io/datasance/edgelet:1.0.0-rc.6 +config: + description: edgelet running on device + host: 30.40.50.6 + latitude: 46.204391 + longitude: 6.143158 + arch: amd64 + deploymentType: native + containerEngine: edgelet + containerEngineUrl: unix:///run/edgelet/containerd.sock + diskLimit: 50 + diskDirectory: /var/lib/edgelet/ + memoryLimit: 4096 + cpuLimit: 80 + logLimit: 10 + logDirectory: /var/log/edgelet/ + logFileCount: 10 + statusFrequency: 10 + changeFrequency: 10 + deviceScanFrequency: 60 + bluetoothEnabled: true + watchdogEnabled: false + gpsMode: auto + gpsScanFrequency: 60 + gpsDevice: '' + edgeGuardFrequency: 0 + abstractedHardwareEnabled: false + upstreamRouters: ['default-router'] + routerConfig: + routerMode: edge + messagingPort: 5671 + edgeRouterPort: 45671 + interRouterPort: 55671 + upstreamNatsServers: + - default-nats-hub + natsConfig: + natsMode: leaf + natsServerPort: 4222 + natsLeafPort: 7422 + natsMqttPort: 8883 + natsHttpPort: 8222 + jsStorageSize: 10g + jsMemoryStoreSize: 1g + pruningFrequency: 0 + logLevel: info + availableDiskThreshold: 90 diff --git a/internal/resource/testdata/edgelet/remote-agent-package-registry.yaml b/internal/resource/testdata/edgelet/remote-agent-package-registry.yaml new file mode 100644 index 000000000..03f5409b2 --- /dev/null +++ b/internal/resource/testdata/edgelet/remote-agent-package-registry.yaml @@ -0,0 +1,27 @@ +host: 30.40.50.6 +ssh: + user: foo + keyFile: ~/.ssh/id_rsa + port: 22 +airgap: true +package: + version: 1.0.0-rc.6 + container: + image: ghcr.io/datasance/edgelet:1.0.0-rc.6 + registry: ghcr.io + username: foo + password: bar +scripts: + dir: /tmp/my-scripts + deps: + entrypoint: install_deps.sh + install: + entrypoint: install.sh + args: + - 1.0.0-rc.6 + uninstall: + entrypoint: uninstall.sh +config: + arch: amd64 + deploymentType: native + containerEngine: edgelet diff --git a/internal/resource/testdata/edgelet/remote-agent-spec.yaml b/internal/resource/testdata/edgelet/remote-agent-spec.yaml new file mode 100644 index 000000000..9b3730af9 --- /dev/null +++ b/internal/resource/testdata/edgelet/remote-agent-spec.yaml @@ -0,0 +1,50 @@ +host: 30.40.50.6 +ssh: + user: foo + keyFile: ~/.ssh/id_rsa + port: 22 +config: + description: edgelet running on device + host: 30.40.50.6 + latitude: 46.204391 + longitude: 6.143158 + arch: amd64 + deploymentType: native + containerEngine: edgelet + containerEngineUrl: unix:///run/edgelet/containerd.sock + diskLimit: 50 + diskDirectory: /var/lib/edgelet/ + memoryLimit: 4096 + cpuLimit: 80 + logLimit: 10 + logDirectory: /var/log/edgelet/ + logFileCount: 10 + statusFrequency: 10 + changeFrequency: 10 + deviceScanFrequency: 60 + bluetoothEnabled: true + watchdogEnabled: false + gpsMode: auto + gpsScanFrequency: 60 + gpsDevice: '' + edgeGuardFrequency: 0 + abstractedHardwareEnabled: false + upstreamRouters: ['default-router'] + routerConfig: + routerMode: edge + messagingPort: 5671 + edgeRouterPort: 45671 + interRouterPort: 55671 + upstreamNatsServers: + - default-nats-hub + natsConfig: + natsMode: leaf + natsServerPort: 4222 + natsLeafPort: 7422 + natsMqttPort: 8883 + natsHttpPort: 8222 + jsStorageSize: 10g + jsMemoryStoreSize: 1g + pruningFrequency: 0 + logLevel: info + availableDiskThreshold: 90 diff --git a/internal/resource/testdata/k8s/controlplane-datasance.yaml b/internal/resource/testdata/k8s/controlplane-datasance.yaml new file mode 100644 index 000000000..da014e636 --- /dev/null +++ b/internal/resource/testdata/k8s/controlplane-datasance.yaml @@ -0,0 +1,68 @@ +config: .kube/config +iofogUser: + name: Foo + surname: Bar + email: user@domain.com +replicas: + controller: 2 + nats: 2 +controller: + publicUrl: https://controller.example.com + trustProxy: true + consolePort: 8080 + consoleUrl: https://controller.example.com + logLevel: info +auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + password: "" +events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true +images: + operator: ghcr.io/datasance/operator:3.8.0-rc.1 + controller: ghcr.io/datasance/controller:3.8.0-rc.5 + router: ghcr.io/datasance/router:3.8.0-rc.1 + nats: ghcr.io/datasance/nats:2.14.2-rc.2 +nats: + enabled: true + jetStream: + storageSize: "10Gi" + memoryStoreSize: "1Gi" + storageClassName: "" +services: + controller: + type: ClusterIP + annotations: {} + router: + type: ClusterIP + annotations: {} + nats: + type: ClusterIP + annotations: {} + natsServer: + type: ClusterIP + annotations: {} +ingresses: + controller: + annotations: {} + ingressClassName: nginx + host: controller.example.com + secretName: controller-tls + router: + address: router.example.com + messagePort: 5671 + interiorPort: 55671 + edgePort: 45671 + nats: + address: nats.example.com + serverPort: 4222 + clusterPort: 6222 + leafPort: 7422 + mqttPort: 8883 + httpPort: 8222 diff --git a/internal/resource/testdata/k8s/controlplane-iofog.yaml b/internal/resource/testdata/k8s/controlplane-iofog.yaml new file mode 100644 index 000000000..e006bf4dc --- /dev/null +++ b/internal/resource/testdata/k8s/controlplane-iofog.yaml @@ -0,0 +1,68 @@ +config: .kube/config +iofogUser: + name: Foo + surname: Bar + email: user@domain.com +replicas: + controller: 2 + nats: 2 +controller: + publicUrl: https://controller.example.com + trustProxy: true + consolePort: 8080 + consoleUrl: https://controller.example.com + logLevel: info +auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + password: "" +events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true +images: + operator: ghcr.io/eclipse-iofog/operator:3.8.0-rc.1 + controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.5 + router: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + nats: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 +nats: + enabled: true + jetStream: + storageSize: "10Gi" + memoryStoreSize: "1Gi" + storageClassName: "" +services: + controller: + type: ClusterIP + annotations: {} + router: + type: ClusterIP + annotations: {} + nats: + type: ClusterIP + annotations: {} + natsServer: + type: ClusterIP + annotations: {} +ingresses: + controller: + annotations: {} + ingressClassName: nginx + host: controller.example.com + secretName: controller-tls + router: + address: router.example.com + messagePort: 5671 + interiorPort: 55671 + edgePort: 45671 + nats: + address: nats.example.com + serverPort: 4222 + clusterPort: 6222 + leafPort: 7422 + mqttPort: 8883 + httpPort: 8222 diff --git a/internal/resource/testdata/k8s/controlplane.yaml b/internal/resource/testdata/k8s/controlplane.yaml new file mode 100644 index 000000000..409669969 --- /dev/null +++ b/internal/resource/testdata/k8s/controlplane.yaml @@ -0,0 +1,90 @@ +# Local E2E ControlPlane - used with: make local-deploy-cr +# Requires postgres in the same namespace (make local-cluster-up). +apiVersion: datasance.com/v3 +kind: KubernetesControlPlane +metadata: + name: iofog +spec: + config: /Users/emirhan/.kube/config + iofogUser: + name: Foo + surname: Bar + email: user@domain.com + replicas: + controller: 1 + nats: 2 + controller: + # publicUrl: http://iofog.local + trustProxy: false + https: false + # database: + # provider: postgres + # user: admin + # host: postgres + # port: 5432 + # password: localpass + # databaseName: controller + # ssl: false + auth: + mode: embedded + insecureAllowHttp: true + bootstrap: + username: admin + password: "LocalTest12!" # ≥12 chars; Controller validates at bootstrap + # rateLimit: optional for embedded + # enabled: true + # maxRequestsPerWindow: 60 + # windowMs: 60000 + # sessionStore: optional for embedded + # type: memory # memory | database + # ttlMs: 600000 + # secret: "" + # tokenTtl: optional for embedded + # accessTokenTtlSeconds: 900 + # refreshTokenTtlSeconds: 3600 + # oidcTtl: optional for embedded + # interactionTtlSeconds: + # grantTtlSeconds: + # sessionTtlSeconds: + # idTokenTtlSeconds: + # mode: external + # issuerUrl: "" + # client: + # id: controller + # secret: "" + images: + controller: ghcr.io/datasance/controller:3.8.0-rc.5 + router: ghcr.io/datasance/router:3.8.0-rc.1 + nats: ghcr.io/datasance/nats:2.14.2-rc.2 + nats: + enabled: true + jetStream: + storageSize: "2Gi" + memoryStoreSize: "512Mi" + services: + controller: + type: LoadBalancer + externalTrafficPolicy: Cluster # OrbStack/k3s; avoids ingress controller requirement + router: + type: LoadBalancer + externalTrafficPolicy: Cluster # required on OrbStack/k3s; Local keeps LB pending + nats: + type: LoadBalancer + externalTrafficPolicy: Cluster # required on OrbStack/k3s; Local keeps LB pending + natsServer: + type: ClusterIP + # ingresses: + # controller: + # host: iofog.local + # secretName: "" + # annotations: {} + # router: + # messagePort: 5671 + # interiorPort: 55671 + # edgePort: 45671 + # nats: + # serverPort: 4222 + # clusterPort: 6222 + # leafPort: 7422 + # mqttPort: 8883 + # httpPort: 8222 diff --git a/internal/resource/testdata/local/controlplane-datasance.yaml b/internal/resource/testdata/local/controlplane-datasance.yaml new file mode 100644 index 000000000..3211aec6d --- /dev/null +++ b/internal/resource/testdata/local/controlplane-datasance.yaml @@ -0,0 +1,39 @@ +endpoint: https://controller.example.com +iofogUser: + name: Foo + surname: Bar + email: user@domain.com +controller: + publicUrl: https://controller.example.com + consoleUrl: https://controller.example.com + logLevel: info + package: + image: ghcr.io/datasance/controller:3.8.0-rc.1 +auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + password: "LocalTest12!" +systemMicroservices: + router: + amd64: ghcr.io/datasance/router:3.8.0-rc.1 + arm64: ghcr.io/datasance/router:3.8.0-rc.1 + riscv64: ghcr.io/datasance/router:3.8.0-rc.1 + arm: ghcr.io/datasance/router:3.8.0-rc.1 + nats: + amd64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm64: ghcr.io/datasance/nats:2.14.2-rc.2 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm: ghcr.io/datasance/nats:2.14.2-rc.2 +nats: + enabled: true +events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true +systemAgent: + config: + arch: amd64 diff --git a/internal/resource/testdata/local/controlplane-iofog.yaml b/internal/resource/testdata/local/controlplane-iofog.yaml new file mode 100644 index 000000000..07ede2017 --- /dev/null +++ b/internal/resource/testdata/local/controlplane-iofog.yaml @@ -0,0 +1,26 @@ +endpoint: https://controller.example.com +iofogUser: + name: Foo + surname: Bar + email: user@domain.com +controller: + publicUrl: https://controller.example.com + consoleUrl: https://controller.example.com + logLevel: info + package: + image: ghcr.io/eclipse-iofog/controller:3.8.0-rc.1 +auth: + mode: embedded + bootstrap: + username: admin + password: "LocalTest12!" +nats: + enabled: true +events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true +systemAgent: + config: + arch: amd64 diff --git a/internal/resource/testdata/local/controlplane-private-registry.yaml b/internal/resource/testdata/local/controlplane-private-registry.yaml new file mode 100644 index 000000000..e01f869de --- /dev/null +++ b/internal/resource/testdata/local/controlplane-private-registry.yaml @@ -0,0 +1,43 @@ +endpoint: https://controller.example.com +iofogUser: + name: Foo + surname: Bar + email: user@domain.com +controller: + publicUrl: https://controller.example.com + consoleUrl: https://controller.example.com + logLevel: info + package: + image: ghcr.io/datasance/controller:3.8.0-rc.1 + registry: quay.io + username: john + password: secret-token + email: user@domain.com +auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + password: "LocalTest12!" +systemMicroservices: + router: + amd64: ghcr.io/datasance/router:3.8.0-rc.1 + arm64: ghcr.io/datasance/router:3.8.0-rc.1 + riscv64: ghcr.io/datasance/router:3.8.0-rc.1 + arm: ghcr.io/datasance/router:3.8.0-rc.1 + nats: + amd64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm64: ghcr.io/datasance/nats:2.14.2-rc.2 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm: ghcr.io/datasance/nats:2.14.2-rc.2 +nats: + enabled: true +events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true +systemAgent: + config: + arch: amd64 diff --git a/internal/resource/testdata/local/controlplane.yaml b/internal/resource/testdata/local/controlplane.yaml new file mode 100644 index 000000000..81c202a0c --- /dev/null +++ b/internal/resource/testdata/local/controlplane.yaml @@ -0,0 +1,48 @@ +apiVersion: datasance.com/v3 +kind: LocalControlPlane +metadata: + name: iofog +spec: + # endpoint: https://controller.example.com + iofogUser: + name: Foo + surname: Bar + email: user@domain.com + controller: + publicUrl: http://192.168.1.6:51121 + consoleUrl: http://192.168.1.6 + logLevel: info + package: + image: ghcr.io/datasance/controller:3.8.0-rc.5 + auth: + mode: embedded + insecureAllowHttp: true + insecureAllowBootstrapLog: false + bootstrap: + username: admin + password: "LocalTest12!" + systemMicroservices: + router: + amd64: ghcr.io/datasance/router:3.8.0-rc.1 + arm64: ghcr.io/datasance/router:3.8.0-rc.1 + riscv64: ghcr.io/datasance/router:3.8.0-rc.1 + arm: ghcr.io/datasance/router:3.8.0-rc.1 + nats: + amd64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm64: ghcr.io/datasance/nats:2.14.2-rc.2 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm: ghcr.io/datasance/nats:2.14.2-rc.2 + nats: + enabled: true + events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true + systemAgent: + config: + arch: arm64 + containerEngine: docker + deploymentType: container + containerEngineUrl: unix:///var/run/docker.sock + diff --git a/internal/resource/testdata/remote/controlplane-datasance.yaml b/internal/resource/testdata/remote/controlplane-datasance.yaml new file mode 100644 index 000000000..b38f83e33 --- /dev/null +++ b/internal/resource/testdata/remote/controlplane-datasance.yaml @@ -0,0 +1,58 @@ +endpoint: https://controller.example.com +iofogUser: + name: Foo + surname: Bar + email: user@domain.com +controller: + publicUrl: https://controller.example.com + consoleUrl: https://controller.example.com + logLevel: info + package: + image: ghcr.io/datasance/controller:3.8.0-rc.1 +auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + password: "RemoteTest12!" +database: + provider: postgres + user: dbuser + host: db.example.com + port: 5432 + password: secret + databaseName: iofog +systemMicroservices: + router: + amd64: ghcr.io/datasance/router:3.8.0-rc.1 + arm64: ghcr.io/datasance/router:3.8.0-rc.1 + riscv64: ghcr.io/datasance/router:3.8.0-rc.1 + arm: ghcr.io/datasance/router:3.8.0-rc.1 + nats: + amd64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm64: ghcr.io/datasance/nats:2.14.2-rc.2 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm: ghcr.io/datasance/nats:2.14.2-rc.2 +nats: + enabled: true +events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true +controllers: + - name: remote-1 + host: 10.0.128.192 + ssh: + user: foo + keyFile: ~/.ssh/id_rsa + port: 22 + systemAgent: {} + - name: remote-2 + host: 10.0.128.193 + ssh: + user: foo + keyFile: ~/.ssh/id_rsa + port: 22 + systemAgent: {} diff --git a/internal/resource/testdata/remote/controlplane-iofog.yaml b/internal/resource/testdata/remote/controlplane-iofog.yaml new file mode 100644 index 000000000..e93295fbf --- /dev/null +++ b/internal/resource/testdata/remote/controlplane-iofog.yaml @@ -0,0 +1,58 @@ +endpoint: https://controller.example.com +iofogUser: + name: Foo + surname: Bar + email: user@domain.com +controller: + publicUrl: https://controller.example.com + consoleUrl: https://controller.example.com + logLevel: info + package: + image: ghcr.io/eclipse-iofog/controller:3.8.0-rc.1 +auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + password: "RemoteTest12!" +database: + provider: postgres + user: dbuser + host: db.example.com + port: 5432 + password: secret + databaseName: iofog +systemMicroservices: + router: + amd64: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + arm64: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + riscv64: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + arm: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + nats: + amd64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + arm64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + riscv64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 + arm: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 +nats: + enabled: true +events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true +controllers: + - name: remote-1 + host: 10.0.128.192 + ssh: + user: foo + keyFile: ~/.ssh/id_rsa + port: 22 + systemAgent: {} + - name: remote-2 + host: 10.0.128.193 + ssh: + user: foo + keyFile: ~/.ssh/id_rsa + port: 22 + systemAgent: {} diff --git a/internal/resource/testdata/remote/controlplane-single.yaml b/internal/resource/testdata/remote/controlplane-single.yaml new file mode 100644 index 000000000..ada14719c --- /dev/null +++ b/internal/resource/testdata/remote/controlplane-single.yaml @@ -0,0 +1,23 @@ +endpoint: https://controller.example.com +iofogUser: + name: Foo + surname: Bar + email: user@domain.com +controller: + publicUrl: https://controller.example.com + consoleUrl: https://controller.example.com + logLevel: info + package: + image: ghcr.io/datasance/controller:3.8.0-rc.1 +auth: + mode: embedded + bootstrap: + username: admin + password: "RemoteTest12!" +controllers: + - name: remote-1 + host: 10.0.128.192 + ssh: + user: foo + keyFile: ~/.ssh/id_rsa + systemAgent: {} diff --git a/internal/resource/testdata/remote/controlplane.yaml b/internal/resource/testdata/remote/controlplane.yaml new file mode 100644 index 000000000..63cdd6331 --- /dev/null +++ b/internal/resource/testdata/remote/controlplane.yaml @@ -0,0 +1,85 @@ +apiVersion: datasance.com/v3 +kind: ControlPlane +metadata: + name: iofog +spec: + # endpoint: https://controller.example.com + ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlETGpDQ0FoYWdBd0lCQWdJV0NBQmhPQkJBZFloM01tbDBGaUlnUlhVVEZvWU9OekFOQmdrcWhraUc5dzBCDQpBUXNGQURBUU1RNHdEQVlEVlFRREV3VnBiMlp2WnpBZUZ3MHlOakEyTWpRd09UTTVNREJhRncweU9UQTJNalF3DQpOVFE0TXpaYU1CQXhEakFNQmdOVkJBTVRCV2x2Wm05bk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBDQpNSUlCQ2dLQ0FRRUFxYlRjZFJmdU1iYmNHQUpNK0NDRk5JU1lKNDhpMENhUURRMWhDRmFtc2I3K0pmMElScml0DQpaTCtsb2d2a0Q4RW5SUWNhS0RmMkkvNDljNkF1YiszSERkMTJCa2N1NGRUNEQ0blduazVJNHJXakxRMGdzSGRaDQpNS3YwMVpUUjE4SG5JNTJaOWtLYnBtaFJiei9SSitWeGpqb2llWDVaTkplZVNwL2JIYk9TeUFROFhFSlA1MEZ4DQpQVHo3SktwUFN3bHE2dDBhWDVFUy9XajNuNXNhQTYybzBpV01vbWFVaTRGeG95RUljekJsbWd4RE9BdEM2UjV6DQpFbkcyeExJcklFWVBCUmJBWlIyNnU5N0lGTWI0eUhnYWJJSkNUM3JWUDYzYkdabmNlM0lhaUUrSzRoZDN5K2wwDQp5RVRPdzMxY2pHa0wwSTEyVzlKY1E2MDNrcStRUVUrK3dRSURBUUFCbzM0d2ZEQU1CZ05WSFJNQkFmOEVBakFBDQpNQTRHQTFVZER3RUIvd1FFQXdJRm9EQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3DQpIZ1lEVlIwUkJCY3dGWWNFd0tocEFvSU5NVGt5TGpFMk9DNHhNRFV1TWpBZEJnTlZIUTRFRmdRVWZTWWJQZnJQDQprZ3NabmlFaUdBeFJGUGdZYlBnd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFZnkzeSt0ZGtnTXVxa3FQZDdpDQoxNWk5elQxeC9ybm5uVjIvRDYxVy9rLzVsZWhSMERRNkxqbWxoNERaMi81b1YxY3JwVHZWUkw5R0dCRUtNdUM1DQo4dzk5NWY3WEQ2eWVabHAwdkJhSlo0OXVHQ1FGMGc5K09kSkNIeHVETU1zaWcxRDk4ZnJYV2tjZkZ4OW9VcW1SDQptQi80VkVCdkt6SVlVREJwZ0lQeWozV24zR1g5WVFtM3J1TjlmNVBvTUsyeHpXVVE3SW5OMjF6WTV4Q2dVajBhDQo1OEhjc002SWdXbW4xU0FaY0h2bGZpaGgvbmtIY3ZDVXA0VzJEeFRNQXFxM2xNSWIwTS9JOWxaYXZaMi8ySUlZDQpFbE1kSkRCdjd6OFUveElnbFZvaWlSM3p6Yjd4T25ZWnRiS0dNQlVIYThoeVI1ekZaWGRrQ25rRFpPc2gxZzh1DQpReDg9DQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tDQo= + iofogUser: + name: Foo + surname: Bar + email: user@domain.com + password: "TestPassword12!" + controller: + publicUrl: https://192.168.105.2:51121 + consoleUrl: https://192.168.105.2:80 + logLevel: debug + package: + image: ghcr.io/datasance/controller:3.8.0-rc.5 + auth: + mode: embedded + insecureAllowHttp: false + insecureAllowBootstrapLog: false + bootstrap: + username: admin + password: "LocalTest12!" + systemMicroservices: + router: + amd64: ghcr.io/datasance/router:3.8.0-rc.1 + arm64: ghcr.io/datasance/router:3.8.0-rc.1 + riscv64: ghcr.io/datasance/router:3.8.0-rc.1 + arm: ghcr.io/datasance/router:3.8.0-rc.1 + nats: + amd64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm64: ghcr.io/datasance/nats:2.14.2-rc.2 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.2 + arm: ghcr.io/datasance/nats:2.14.2-rc.2 + # routerSiteCA: + # tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURPVENDQWlHZ0F3SUJBZ0lSQUtqeDNlUitiWEpmQ2pTSkZ6T3kxc2d3RFFZSktvWklodmNOQVFFTEJRQXcKR1RFWE1CVUdBMVVFQXhNT2NtOTFkR1Z5TFhOcGRHVXRZMkV3SGhjTk1qWXdNekV4TVRBME1ERTRXaGNOTXpFdwpNekV3TVRBME1ERTRXakFaTVJjd0ZRWURWUVFERXc1eWIzVjBaWEl0YzJsMFpTMWpZVENDQVNJd0RRWUpLb1pJCmh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBS2pwZlRHUVFJRFZRYTlRenAwQkxSZGI3TEZRS010eExiemUKY0hXeXAyUUp1VXZudm1LRG1rN3FuRkVYaVdpdFRLOVJPRCt6TWExMXF3M0lkb3ljNlhVZk42Y25UT1Z5YkE1bQp1QnZMQmV5eWhTcmVFYTV0dzVZVFNzbVhqRXJZek5LVzM0UWZIcmFvSTByYmhZL2Y5UXNwZUhXSkdiWlRiRlUwCis2N0lma0dCUlZGZUNRS3BEcEVMNVJ0cUpZOHFzanhXSlJ2NGYwaVE4UGYwUnhCSS9iZ0pBem42YS8vSVNFeUYKRkUxN2RBMXMzeGJsdFBPWU5CYll1bUtFd1lpTnRvSE9veWt1c2dodTFMOWVjU01WeHFlVjQ2Uk9BS0RRekdNTQphMXB0NVlNemVFNGZGcnhqY1l3SEhhSjN0ZEhCQkc4RWFmek5tOXk0a3Z2enZuK2JSUnNDQXdFQUFhTjhNSG93CkRnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQkJnZ3JCZ0VGQlFjREFqQVAKQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlNUVjd5K2xXQ2xNRytpRTJrTldrZjFQQWFzN3pBWgpCZ05WSFJFRUVqQVFnZzV5YjNWMFpYSXRjMmwwWlMxallUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFvQ2czCnY2a3U0YzBHYkIxV2ZXRjZnT3EzOXF2VFlmYmsxakpvdGxYTkl3dUowdTBxQ0IrTDFXYzN3MWNVM3NpZlNiSnoKY21VSWZ5Q2oyeFZRYUNGa1dDSkxtN0V0MXIrd0VYQ3V4VlJJSTRYcTdhSkVsM0w3WHFvU24rM1k4RDI1UjljLwo1OFQzNmpXcmhmVHdqWGs5Y3lDUTRxQnpkbmF5TGpqd3oyRU9BVnpFQWYyREhWZ2MrV1l2QVQxNmo5UndYMzFRCm9NWDc2azBCbHZtY2dzZGNlTTkrNmdHeGFEZnQrOUQyb05xdGNvMnhGWFBUVVRGalg4Mm05NXArbUk4UnBOMEsKRHloWFVheVo5anFKOGRORTU4TzZBUDJrelE4YWNwNHRQTGsrL3EwRjNmNnZEOGdVVHk3RWRwQiszekdFVWpwdApBcytrclNwTWV0REZRbEU0aEE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + # tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBcU9sOU1aQkFnTlZCcjFET25RRXRGMXZzc1ZBb3kzRXR2TjV3ZGJLblpBbTVTK2UrCllvT2FUdXFjVVJlSmFLMU1yMUU0UDdNeHJYV3JEY2gyakp6cGRSODNweWRNNVhKc0RtYTRHOHNGN0xLRkt0NFIKcm0zRGxoTkt5WmVNU3RqTTBwYmZoQjhldHFnalN0dUZqOS8xQ3lsNGRZa1p0bE5zVlRUN3JzaCtRWUZGVVY0SgpBcWtPa1F2bEcyb2xqeXF5UEZZbEcvaC9TSkR3OS9SSEVFajl1QWtET2Zwci84aElUSVVVVFh0MERXemZGdVcwCjg1ZzBGdGk2WW9UQmlJMjJnYzZqS1M2eUNHN1V2MTV4SXhYR3A1WGpwRTRBb05ETVl3eHJXbTNsZ3pONFRoOFcKdkdOeGpBY2RvbmUxMGNFRWJ3UnAvTTJiM0xpUysvTytmNXRGR3dJREFRQUJBb0lCQUFGckhJV3dYQlQ0NENQLwpFMkpzSW5CM2NYc05CNXFyRTZMcURKc0xGSzdFQ2lOTXRJNDlPVmJVK2krNnpvanJma3V4UWVpcHNqbHVWZHU0Ckd1UVVEcE1tTlBYRUN3MlkzV0Z0bEE3ZnNKSzJtUThDd3d2cVEySGM2RWJkd3BiVStwQ3Jld0Jhc1RaVmM4a1YKZU5TbXk4eFJNb0FYZ1BqRlVKRTlSYWtkRStVQms3dTJRc1R0QXNQNFlBUWJJMUdna0FDdGNLTUNaYnBSOXZyNQovVWc4bzJzSXBWMHJRL3F4ZVBqUGtwV3lWL25xV2hZRjI5MGN2bGVoZDBFQlVWOE1qWWduUlBuRmFsaXNaMm1iCjJzNldIZDAvMXNrVkdQS3ZsdTd0NEJVR2xMS09tSW00L09QVVRQRUJpd2liNWJycDZpVENQNkYwdWFQS09VQlIKa016VlB4RUNnWUVBMEREMmNMbyttVDhEMUIwSWxqWWx6L1UwekdKNXJaSkxzZ2F1UG5icmg2NU82RGhpUmtOTgpXSUpIQWVSNUdhV2tkOGZSS3JmRDhxUlZqOFppUGtZZDZoVjhMcHJUNHNuK1dwM1FFRE9IYlNQalYxRWdKRWYwCnU1QjFIZStjS0lVL0cvamRlVFF0RjRmNmp3bklyamhjVUs2MENMekJRcnJKbTZaRkhXaXdVZ01DZ1lFQXo3Tm0KNStWSUJpY3JsTGxQRU1aank5YUI5TkJGNFZsaEJEUDJYbVZOZnQrdE5LMUNwRWhKZVZscTJzWTdhVk5SVVpUaAp1OTJVNzQ3U2RGcHZQd1RDVkR3eER0SEhJOTB0NnRvYStqelBhMVN5cjk2aFhVejY4Ym1QaTFUTmhvQ3Z4L01yCm4xS2crNWtrNGZKV3pKd3gwUTNRdmMxTGtTM0lCdzJxWXpEeUlRa0NnWUJYalkvR1BuemU0NkpQak5vMG1aYnoKU3RLbWRXOW9jRkxIRG9vdW1NSmFjQktkRkVFMy9VdkV3aHpzamRIajJFWS9YVmY0bUFtZXZEK0RWRkd5a0xnNQoza2s0TEVLWmFJdEFQb2ZtbUZVR3NBWUdqWVp2MjVidlhrUHlqL2JqRDQ1SHpEUVBxY0tnMTcybWM5M2licTliCit1eVpsQS9PYVZFcDFSWFIxVm41VXdLQmdHY3lLZVQ2SklqNkdVc3hyemtVZVMwa0RUbkg2WkNIeWc0K2l5Qm4Ka05PQzZ4b0xJOXRnRnpGMTNnT0pEcWZNUDlFYStmVlBxTnBGeWdjSmo5QnQydWZqYURTR3dqenRmZ3o4QlA5awpDMksybUhtTlVmdDdiZ3VBT1BQdlZKYUpoYzBBNHlHcitsUkh5TzJDYk9JSWtTL2ZmMkZ1aVNjKzZlMm5Pb3RDCkhHdVJBb0dCQUpvZ280RXNBNnZBRSt6UVJ5T0pYaDJ1b2ZHaUZRSDF6VUladThSRTJCNHZmNE5IMEppZUtxVGEKQ2hvQ0luZzM2VlBQZUhxSjdWZUpXdVBXTnRobHdsV1QxM2FiQ2JrdzhKRVhjUGFpMnNYM3RhVjBLNEpMbFVkaApoeWhIeEgvQzB5clpEa2dmR2RFVlp4SjZ2emNBcUxlakVidHZuemFsSjNLRiszNzRnYk12Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + # routerLocalCA: + # tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURWVENDQWoyZ0F3SUJBZ0lRVEhQRTdjWHlmM042cHc4TVJzc3MwekFOQmdrcWhraUc5dzBCQVFzRkFEQWkKTVNBd0hnWURWUVFERXhka1pXWmhkV3gwTFhKdmRYUmxjaTFzYjJOaGJDMWpZVEFlRncweU5qQXpNVEV4TURRdwpNVGhhRncwek1UQXpNVEF4TURRd01UaGFNQ0l4SURBZUJnTlZCQU1URjJSbFptRjFiSFF0Y205MWRHVnlMV3h2ClkyRnNMV05oTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF5d1Rjcy9kd0RXRWcKZS9OTGVPSDBjb0NsNEs0NFhwN0lPUkc2U0ZQVG9sQXJnWm56cUxMRnRKSThzblMwMnBlcDFRbFZXRWs4aU5Qcgp4UGc1M3VJeVJTWTc1MjRIb0oxQnlKaGxGa200SzVLWmYxbVFNQVd3VUpIQ0JlTG84VnZrd0Fjd2xYSWo0NUwvCkg0eTZSUXF3azMzTzFhVzMyVXJPcGZtVlVDQ1Z2MVROSElGY0UvTTdxVnN0Z1FYN3Zib2J6cVR1ZmZLMFNkM0sKTGFHVU9pbEVwRFI0T1hlalhrS1gyazZ2QTRLY1Mwb05NbnJ2UkN4ajA3L3NZeXE1Z0F1d1BDTkdRcWoyMDROaApKazl3NkF3cnZJUHhsN2IwcmRzaS84T1FONncvaFZjOGEyNFQ0ZlRpUVpBazllZVNHUUtkMkVvM3J1RytXdk5QCnZldzJua3JFN1FJREFRQUJvNEdHTUlHRE1BNEdBMVVkRHdFQi93UUVBd0lDcERBZEJnTlZIU1VFRmpBVUJnZ3IKQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVK0hXWApkMWxQNkNkVGttNWRtUUJzanowMHJxRXdJZ1lEVlIwUkJCc3dHWUlYWkdWbVlYVnNkQzF5YjNWMFpYSXRiRzlqCllXd3RZMkV3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUtNZlNQa0tZOGZidzdLd1VmTzBhZGNvYUYxdXlyYkwKVittdEd2YWJJbEVUNTJQQWYySm1pU25aM1dobHh0TWMveXNSRXZTcjBjMXcwek1LQ2FyWnBRQWRIS3UyN1ZhaApJcHRUZlp3TzFiRjhwcHRjVkZFbDI2Qyt2RmlPNzBMK2lhMWxGYnJTdHpTSUNwTEZYTjdBSS93akcvWjltVXBHClF4MUdpNGQybSs3ZmNVY2hiRzVpdVpUckkzaXFkRnJTWWJ0SlozdnhiRGhLT2YwalNwUG9Tdk96a3FwbnA2SXoKUFNMejhqeE8rYWljalk2MFY1WS9STldCNmNRd0F3UVBFa3F5aUJ3ZytxbXNlMWQrNi9YRmVoTEhtNVhjVFcweQoxNW91VGtjeWdPenZXWjE2VHZvTGdjWFZxVFgveEhLa0pJVklaWkpUN0srZnZ4T3VDQWFCRndBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + # tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBeXdUY3MvZHdEV0VnZS9OTGVPSDBjb0NsNEs0NFhwN0lPUkc2U0ZQVG9sQXJnWm56CnFMTEZ0Skk4c25TMDJwZXAxUWxWV0VrOGlOUHJ4UGc1M3VJeVJTWTc1MjRIb0oxQnlKaGxGa200SzVLWmYxbVEKTUFXd1VKSENCZUxvOFZ2a3dBY3dsWElqNDVML0g0eTZSUXF3azMzTzFhVzMyVXJPcGZtVlVDQ1Z2MVROSElGYwpFL003cVZzdGdRWDd2Ym9ienFUdWZmSzBTZDNLTGFHVU9pbEVwRFI0T1hlalhrS1gyazZ2QTRLY1Mwb05NbnJ2ClJDeGowNy9zWXlxNWdBdXdQQ05HUXFqMjA0TmhKazl3NkF3cnZJUHhsN2IwcmRzaS84T1FONncvaFZjOGEyNFQKNGZUaVFaQWs5ZWVTR1FLZDJFbzNydUcrV3ZOUHZldzJua3JFN1FJREFRQUJBb0lCQUV4UElBdnZLaW1GUS9vRApHVyt1OHJ4bE9iUkpsL3VNMERLUFJNY3g1djhBQmxKWkJScDRVOUxMRXRCN0NJMlBhekVkcUh3ZVR3Z1pLK29sCjZVNnJFLzBrNFdoY1ZiYWIxV0dxVW5pOXJlR0c0WFphT2xXcWxicTdCc1JDcFk4dkhMekhGdzVkVURzV2dobWcKUWxWNExxWEpxSWhxbVQrdUhCMkx3Z0ZUdGlXcmRWTnBGU0xLaUJLY2d5STNRWDJ2dGhvVWdmaURTY2NIRGxKKwp0OHMzczZ1MXdROW5jd2hMSGZ6dHVYREoxemhPS2kwTUIrYncyOTZBa1piclFtSE1VVjgzdWlQditQWkcybHozCnNLeTlIMTlraGxMVkh2TmpGVWRIREJ3cFdCR2RTZUREQkwrZUViTlFneHZHWmRkQ01hbzMrRjZkdG52SDVkMkMKS2JIUjc2c0NnWUVBNjFiTUxIRndkd0lkSmJIMm85ZXhmTytLTHhtQldYMUN5Y0JkK3RIZE9odHBwanNqYTVlbgpPSjdsTnU2a0c5ZGtnLzJuMTVsSWFYdEFPM0RkU00yZDlzNUplMXFxc3dCTmtvdjFPYk1CVjFaREpBYUN4UkU2CnFRMkxYSEphMTk0NVI3V3BRVGRKYldVWEY0OTJvdUMzaFpObFNOVFhqdHFEVnRubDB5L2xCVE1DZ1lFQTNOZXQKVll3anVKeTliWVF6SkpKRGxzaUJVeWFOdTdHNk5pSkxFQ3Fyb1pQa1gwMUU2UjBUTHJoWXZSNG0yOEltK0xyTApvWnNEZFZTS09tUURFY1hNUWJBa2ZUWVhMOVV2eUlFbWdIZ0Vsb0h6SjMrNEk5SlZqUXg3bWwzNkYxWUFRUy9UCi9ZM3p6MWZyNE5SZkV2RWxuWmFib1RCVG5ldW12UWdnc0hoTXpWOENnWUVBNm1mSTlCZUZtclFiVGhtRmZjcHcKZWUycDZLSHg2YTNQWVY3ZS9ONGVDU3VXdnNFMjFZcjNQM2xjKzZzVkFMbzQzeE0vSTRzRXlqTytWYlprWW9pVApaMm92WE5PQkpNd1BlQUU1bjJBQjNQa0o1UThySDVpNm9mbmdycE1ra3RGQW9vRjU5WUJZL2NKc0RzYVJ0MGcyCjQ3QmRlUDZ2T2hYQ0xqYlpLTklTdm1zQ2dZQWRuNVN4ci8ydXF0L0NEQVNzT0M1MjBHaUFsZUJYT0F6cGJBb3oKbmZXdDA5L0RaT01FZmhEdnFHekcyWCtPNU9sRFhoTW9sMW1NYUkydUxYSTM5UmRrREZPb3RCUENKOCtrRHFieQpmcWJtNVlHUFg5TjhncDlWTDBKNVAzZm5uM0tqUzk0YzJlakZmRjY0cHVRbDcxRURaWXQwd0wzR3BqQ1VsTDJGCnptMUc4d0tCZ0hQSUFhbURGQ1hlM0ZGNjloUWp0QTRCdUNRRko0UlVmWDhScFRadTJ1cE8zTWk1d0Q0bFQyMEMKOHFicy9lTUdCdDVOL3NLMEYrY1RoK0hEd3lTdVIzWHhmZGFyTlNKVytDOUdQVmN0ZVNHbjdpOEV3enpZNEt6NApPSzFseDZpaVdhQ2hLUGV1Sm43SHIwZ0h6VzVSaEZ4QjB1NnM3N2JCL1VVbFFlUGNaNlNmCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + # natsSiteCA: + # tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURNakNDQWhxZ0F3SUJBZ0lRTmVmcTFGZVpOQWdTbStZSHltTnE1REFOQmdrcWhraUc5dzBCQVFzRkFEQVgKTVJVd0V3WURWUVFERXd4dVlYUnpMWE5wZEdVdFkyRXdIaGNOTWpZd016RXhNVEEwTVRNMldoY05NekV3TXpFdwpNVEEwTVRNMldqQVhNUlV3RXdZRFZRUURFd3h1WVhSekxYTnBkR1V0WTJFd2dnRWlNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDM3FobW91Um15OWFCMy95QTJQV1U3NytQUGFaZ1hYbmdpSk1mTDhEY3AKVXZmOEYwM20rSzhONzdBYzVlRTk5SFJpY1lMUUwyL2x6Q2h3VE5RNVBmaGp1NC9vaDlxMk5pSExXR21OeHBTbQo4aVRzV3FMS1VmUEVkZ2lDcmZNTTJOd1YyL1V0MjlKTXEzSzlYdTE1V0g3dnFIZUZQOGR1cWYrNEZTM1dlVFJkClZBdkovUi9KQWM5b3krTXpUWExpOXRmN1I5d2xNa2ZDQkJURUZHanpNRlpTZXZkY2hHcXk3Z284MUhCbm5qSXUKemc0NkNrUUVSbGE0eFpJdHlGVU5BRDNVNzlmejQvdVFZK3RCMGxOZytMVVk2K01pL0ZzK1BPSGlpVmw4eWMzUgpId3drMWpuNWFiV1V4NHZLZVExYVdyRFpxd3pJQ3RDMWdLalliNkdNeDBmTkFnTUJBQUdqZWpCNE1BNEdBMVVkCkR3RUIvd1FFQXdJQ3BEQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3RHdZRFZSMFQKQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVgxOXNQSVcxZFRJTG5GYnA0YW8rQUtCcUNNTXdGd1lEVlIwUgpCQkF3RG9JTWJtRjBjeTF6YVhSbExXTmhNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNaRXJ6VllzUUpDdnRlCnA4bVQ2RGlUNUhLTTJ1VmNnYkNzb2E0ck02bjRYYVp5bFAxMjZSZEJUSFhvWnNROE1pUi9rSmJHRkdBTEs5djcKTjB6ZUY3ZlFuUGV0b3NqWnVVV0pnYTF0aGdzRmZ4Yzg3TmJxSTRsN2FJVTFYa0FzN1ZObXZJRGJPYmZZVkoyVApPQkJXaWtvbkJ5aXVtOU9ncDUvT2FtRjFqVHRZS2duTGhUbzd4YlFqcksvdHJ2bWVuYXl4SWFlVWU2NHdNaFgwCmg2djJJTDQ3dnQ1b3lMOUJ4ZGcvaWorZlRLcXA1RnFMZ1dpbW5RUWxyMkhOTWlmMlljMWRKM3Nmb1EwaDZvSFMKRytpZm5aNjFHVjc1alh0ZVR6OGVPQ25oUDY5bnZKU2QvUTA3NHpvQUs1QXV1VTdFckVLYmdRQ0FEN3hxTTBlQwpqV3luNTJqcwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + # tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdDZvWnFMa1pzdldnZC84Z05qMWxPKy9qejJtWUYxNTRJaVRIeS9BM0tWTDMvQmROCjV2aXZEZSt3SE9YaFBmUjBZbkdDMEM5djVjd29jRXpVT1QzNFk3dVA2SWZhdGpZaHkxaHBqY2FVcHZJazdGcWkKeWxIenhIWUlncTN6RE5qY0ZkdjFMZHZTVEt0eXZWN3RlVmgrNzZoM2hUL0hicW4vdUJVdDFuazBYVlFMeWYwZgp5UUhQYU12ak0wMXk0dmJYKzBmY0pUSkh3Z1FVeEJSbzh6QldVbnIzWElScXN1NEtQTlJ3WjU0eUxzNE9PZ3BFCkJFWld1TVdTTGNoVkRRQTkxTy9YOCtQN2tHUHJRZEpUWVBpMUdPdmpJdnhiUGp6aDRvbFpmTW5OMFI4TUpOWTUKK1dtMWxNZUx5bmtOV2xxdzJhc015QXJRdFlDbzJHK2hqTWRIelFJREFRQUJBb0lCQUNYTHhzZUN2Zy9tdmR6cgozd1ppZUphT3piZ1ZrQ1BCQUdKd1pNQnFnUU9MVERhdjJndDk0bEp4SUxJMXVYWmRPK1UxWEZqNDVpTnBjZW40CldaVWxGRng3MFFmbWkwTURuVTFDTnNpakZOek5TSDF1UW9GMXYzOU8xZjRFaTVlNWVnTXlsb0JYTkM0c2V5cU8KNGpwZVZKTC92WWJwb3VJNmNFSkM4NEduUkRndk5jSjNLTEdMaE0xVVl3UWlubjd0QllCcDYrWjMvcExCNmRyYwp3SVhpT1A1blY3cWNjaDVwaGNaMktBVVhzYmNqcFluSEZrdE1sVlFkdWlQVHJ0UmdLcFBpMHVjRThqQkR5Ym5OCmtoMzd2elB1TUM5aFZSKzlodXBWNEoxbUNlQW05WVFqa0dobWcrM1gxZjh4d0JINFpPK1c4QXBuZUNhQ0VtdWEKbkN4b0NWa0NnWUVBN0NzTHRFZnk0VitvOE9WdDhVL1NaRE5VUWxBUkJhRzUwamhhSEtnd3hDQ3J2TWp1WHNteQpxdjd5UFRLc0s3YTBJVnBrUHliWldqdkI4ZVJybHBueWZPR2JOUDhJK3dlR0NWWmRaQXJkb1dCb25DYzU2MUs1CjNRaDFwMnJQVmd0TjdCS2Y4WEkzeVlsdkwyajI0SC9JRmhTNy81L0tjT0FOWUx3NmNoWG9LdVVDZ1lFQXh4WmYKUFZzamVJYzh0UnRzRTV2YXlBWEpHc3B6Tks2d3M1TnlTeVJhRlJiWU1Mc09GaVVVQlFiN0N1UFMwWVMyWUxEMQovZWtNcmtWWmZuWFpXSHZqeGFXLy9hL01ienhzYmdDUEJabkpoMUMyeW9lQnBMaDdUZ0p3dzJaaHdxOUl2VVVrCms4emZoclJMbUJLYldETG1RaHRIV2U1bzNHM3MzREpOUDdGN2tza0NnWUVBMmtUbWFsWmMyWUxweHNxa2svUXMKQk1PVHlqM3BuWVRkRXJkV1FVb0kyQnRCM2hidWg5aHVNcSt4L25HSXdsWDNvU1BEcHNJbSs4aGk5VWNoVUcwegp1Y3RoQU5mODJ0VVhRaVg1NW01TWE4dUlvMWwxcEZJdXlXUDZLU01FUVFmdG1wT1VFemgyNnVNRVNaTC9LSG13CjJRZU13VEpUallMbG1sUWN5RGdLL1NrQ2dZRUFnQUFLUzlDRkJjRXRidU9xb1JEYm9TN1hGYnFFUjZMcFNRdkwKdURRdkZ0QVJQNE9Fa3doVHpzZW1NR0k1OFN0NmRzQlA2R2dtRndYUGZGY1kzcU1JMXRLeWxkQ3BoL3M1VzZCUQpWREdFT05QVU1uTGRENkxzNUVMOWJTUXVScFdjRnRTVnA5RlpCYXAxejlobXVGWkJaTTlWR0tVSUZuRTJrSHhtCjNrU21Sc0VDZ1lFQW5yK01ZR0MzOFNuZVQ3dExSZm1QVEdEQXk4VWs2dm51ei9ySUN2YU4vSlNDbUFyS0wvSG8KQ091VHpOUUs4Y2twYkdQZGN1NnZWS1Y4bWJ6d0ZETCs4czRMZjhmRlBSajc2YVFiNEp3MTNTVHZlU2hSa2xaLwo1aGY4N3ZWWVd2SXkwQWovbExxdGdSY1p6RXpzWWdQQ0pGenlsU1Azb3BSVFg2emtYZ2V6NVpNPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= + # natsLocalCA: + # tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVRENDQWppZ0F3SUJBZ0lSQVA4SlI0R0lYZm4wOUpKaWsrMkVIaDB3RFFZSktvWklodmNOQVFFTEJRQXcKSURFZU1Cd0dBMVVFQXhNVlpHVm1ZWFZzZEMxdVlYUnpMV3h2WTJGc0xXTmhNQjRYRFRJMk1ETXhNVEV3TkRFegpObG9YRFRNeE1ETXhNREV3TkRFek5sb3dJREVlTUJ3R0ExVUVBeE1WWkdWbVlYVnNkQzF1WVhSekxXeHZZMkZzCkxXTmhNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXNGSW1NZFJvU1VROFpOcTkKbDVRNnB0RWVub2hxMTdQWHIxMHdPVlZvd2J3VHBrUDZBdFgrMnhWMDNUbzF4SEoxdWZoOXczbWdKd2JaQW5iRAp3amdIU2ZmZkIyRjM2bWZjeVF4RGpZdTY2cGV6SVNzMlJNNVFwZEZhcTdoN0k5clIyNUorcGRPWnpzM29vUEt1CmFWWHpFUVVVUE1QNkxBem1pZndMVnAyUGFlZGZaRENxR1laL202eFA4cGtSSmdXc21RWmdWZVJ6WjdBMzNXSDkKRVFmTnc2WHZmN3NmQ0lvRUFVeFozaVZLd3hoMjJhaVpsRW03aTdYZkdLNVVBRFhVbG54V214UVFlM005dWNXMgprY21vbGNnKzA4dlc5bzFhSzdkNS91VmVNNExSbGVKSVF2R3c2YkFmVElNU0Q5L0gzdmg2M1diVis0N1krT2xtCkx6ZXJRUUlEQVFBQm80R0VNSUdCTUE0R0ExVWREd0VCL3dRRUF3SUNwREFkQmdOVkhTVUVGakFVQmdnckJnRUYKQlFjREFRWUlLd1lCQlFVSEF3SXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVNcjdyYmJ5VApBanNmVnVSQmI5Mm5SRDcvYkZVd0lBWURWUjBSQkJrd0Y0SVZaR1ZtWVhWc2RDMXVZWFJ6TFd4dlkyRnNMV05oCk1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ0hmbmdaeXJwMFJtaytMaG9qdkZrbVNyWlBkc0FFakpDWTNSSVgKTEVYZ2FzNTk5V3lERXJIaEo3MXNDTTdneWJGVXdKV3ZyOXliY2NZdVJqa2V5RE11bC9USlZjUERwc29zOGhrNgpOaElCa3pwRlZLdDRmL2t5NUpNMVJHdWxKRXdwNGNqazRMblJ0VFV5UGJpSm5XR3oybXBOaWFHVlI5bTluUW5QCk1mS0Q4dDJnZjdBV3pGS1poTWVRclZYMldmWHowKzRRUjJXck5NSjVnb0JHR1NCYTNrYU5LdE1ZS29BV25WYUEKME9FcGQ5VFNKZFlNUURwcVpTSVFhMEQya0Uvbi9PanNXTktyNGlzajVteEY2OU9vaHZhYm9YUFlORVI3YWl6OQpTc1B1Rmc2bjZLaXVYdkZnMGdFdE5Mb1dCZlBNaTZJOEhtY0MvdWlyNTlRU2JXZEYKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + # tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBc0ZJbU1kUm9TVVE4Wk5xOWw1UTZwdEVlbm9ocTE3UFhyMTB3T1ZWb3did1Rwa1A2CkF0WCsyeFYwM1RvMXhISjF1Zmg5dzNtZ0p3YlpBbmJEd2pnSFNmZmZCMkYzNm1mY3lReERqWXU2NnBleklTczIKUk01UXBkRmFxN2g3STlyUjI1SitwZE9aenMzb29QS3VhVlh6RVFVVVBNUDZMQXptaWZ3TFZwMlBhZWRmWkRDcQpHWVovbTZ4UDhwa1JKZ1dzbVFaZ1ZlUnpaN0EzM1dIOUVRZk53Nlh2ZjdzZkNJb0VBVXhaM2lWS3d4aDIyYWlaCmxFbTdpN1hmR0s1VUFEWFVsbnhXbXhRUWUzTTl1Y1cya2Ntb2xjZyswOHZXOW8xYUs3ZDUvdVZlTTRMUmxlSkkKUXZHdzZiQWZUSU1TRDkvSDN2aDYzV2JWKzQ3WStPbG1MemVyUVFJREFRQUJBb0lCQUE0cEJzcTlMNFBrMEVIYwpSRm9xVEJ5T0VscXlnM3dmeEJ4Z0RFbXFkNCtKaW4xeG02QkRMZVRMNEJjTlAvaXZKWS9DS2wxNnhOY2xpR09YCmhLaXlKYm0xeDZwWTFFL1Z1QWhJYlJ0dXc1dm9BM21RTmh0SUEzZVJyT202RnQrZUNQa01scEc4UU8rNEh5emYKMkl4Nm05cjcwTENCbjdPT2RLeFR1dGhocm4wWGRzODZIR21TTXpWWGFJZDBHeDA5LzV0c3BtZFBpWEpUWnluTQowR2lULzVHVHdWQUlLcEU4cjNRRjJnbWRqN2pBK3ZxT0V2eFNyYlcwRjVlT0hjRmRuMHBMUlNLWEVBRVVhaWNYCndGRVR4eWcrMHFtR3orcGZPWFQzbVFDU2kwbCtQSlRCdG1kVVZoQWg3cTdFODlEaWNHeW52UndGVXVKVGxheDUKTm5kUVVMMENnWUVBd2J3dUwwcFhnWXRuMDBLUThxN2gweGhnOVJZd0ZmTnREYzg2YSs3clRzcGlJTmZ5MDdQQwpyakhZS0dVejNZY1lFL1EzZGUxczQyUGVCM0NWaTJmeVBEZzJKSE1ySHpKOXFCY0pVam9GUzFQOHVRdlBjQlpxCmp2S1BYN0M1OFpKbXRkT0hsUXN0RG1OS3l3YjQ4czEveXhvQVh2M1ptN3V5RDFPZW9wWjYvQlVDZ1lFQTZQMHgKWDR6bEpRV0FIT0FPYWh0TnlLMVlYdWlZUUZOelRpOHVnQUZlbWdOdFJ3a1dCdWFNMENQTnZDZ0tDS3QxNEFHdgp3cHdMYlVuOFBBUXZWa2dRWlJneHhWdGZhU1kxNlIvYVJlRDJHVHZwSDc1YlRMWDd1SG5JaENMSUFDWnFtS1A2CkhvYWpFU0hHRi9ZaEtyV21SS0VReDhhVUpxMVRqV1FmQVFDVWdYMENnWUJXdUgyVC9ac2VDZUQzMkJ3NkJiNWcKVjlGTzVCZXlPN3pkS1ozbElwV0NOMldsZmdUY2J1TCtScUdUczNsNytEVDIrYUs1endXbTQ5VkhUMFlobU8zOQp0c3ZGbFNnQVZ3R1lkSGRmcjBrZlp3RUJkQi91OUpuT1V4V0twL2tVQVl5b1ozK1JYK2RUUVc4QllxV2RTZytpClFvbFgvQm1rZEdoSUpBNG1pV1dUNFFLQmdIWk1lTkZIUE9IN1ZQMVVWbjFSdDhENUl6R3RjQURaWG1hSVZsZncKV2hSaFFROGNjZTYzQ1RCMXZYU1g3K0JQRHQ3YWZGK1gwOFYrRjNCeHY0ZFR0OTljMVlpYnlHb2ZXS2d4NENZeQovMEg0eFhtMHNhN1ZpQ1kyejdVbjQ5MFBwSGcwYWo4dHBZYUJXNCszRFVnZVMzbjFQZ3Z4ckMrbk9oRkVrT2wxClhmSVJBb0dCQUlvb0ZyZ1pKelFHR3NEcGJ4VXJUVXBLK2xVekF0c0FFcWN6WnJWRm4rVlRFZ0dvMGRsdjRTVE0KK1lhRTdCUHpMbE1IeWZQdzVzMUpCbHVLVXIvcEpzOGZLWnlEaVVOcWR6S2NDZkJqNVJzd0NHV3IxbFZUN3RqQwpUeURiVFg3UmxWWVp0Y0FEVGxFa3kyYW9admN0OTIzbmEzM0R5V2VNUjBJUkg2S05VcXdLCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + tls: + cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlETGpDQ0FoYWdBd0lCQWdJV0NBQmhPQkJBZFloM01tbDBGaUlnUlhVVEZvWU9OekFOQmdrcWhraUc5dzBCDQpBUXNGQURBUU1RNHdEQVlEVlFRREV3VnBiMlp2WnpBZUZ3MHlOakEyTWpRd09UTTVNREJhRncweU9UQTJNalF3DQpOVFE0TXpaYU1CQXhEakFNQmdOVkJBTVRCV2x2Wm05bk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBDQpNSUlCQ2dLQ0FRRUFxYlRjZFJmdU1iYmNHQUpNK0NDRk5JU1lKNDhpMENhUURRMWhDRmFtc2I3K0pmMElScml0DQpaTCtsb2d2a0Q4RW5SUWNhS0RmMkkvNDljNkF1YiszSERkMTJCa2N1NGRUNEQ0blduazVJNHJXakxRMGdzSGRaDQpNS3YwMVpUUjE4SG5JNTJaOWtLYnBtaFJiei9SSitWeGpqb2llWDVaTkplZVNwL2JIYk9TeUFROFhFSlA1MEZ4DQpQVHo3SktwUFN3bHE2dDBhWDVFUy9XajNuNXNhQTYybzBpV01vbWFVaTRGeG95RUljekJsbWd4RE9BdEM2UjV6DQpFbkcyeExJcklFWVBCUmJBWlIyNnU5N0lGTWI0eUhnYWJJSkNUM3JWUDYzYkdabmNlM0lhaUUrSzRoZDN5K2wwDQp5RVRPdzMxY2pHa0wwSTEyVzlKY1E2MDNrcStRUVUrK3dRSURBUUFCbzM0d2ZEQU1CZ05WSFJNQkFmOEVBakFBDQpNQTRHQTFVZER3RUIvd1FFQXdJRm9EQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3DQpIZ1lEVlIwUkJCY3dGWWNFd0tocEFvSU5NVGt5TGpFMk9DNHhNRFV1TWpBZEJnTlZIUTRFRmdRVWZTWWJQZnJQDQprZ3NabmlFaUdBeFJGUGdZYlBnd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFZnkzeSt0ZGtnTXVxa3FQZDdpDQoxNWk5elQxeC9ybm5uVjIvRDYxVy9rLzVsZWhSMERRNkxqbWxoNERaMi81b1YxY3JwVHZWUkw5R0dCRUtNdUM1DQo4dzk5NWY3WEQ2eWVabHAwdkJhSlo0OXVHQ1FGMGc5K09kSkNIeHVETU1zaWcxRDk4ZnJYV2tjZkZ4OW9VcW1SDQptQi80VkVCdkt6SVlVREJwZ0lQeWozV24zR1g5WVFtM3J1TjlmNVBvTUsyeHpXVVE3SW5OMjF6WTV4Q2dVajBhDQo1OEhjc002SWdXbW4xU0FaY0h2bGZpaGgvbmtIY3ZDVXA0VzJEeFRNQXFxM2xNSWIwTS9JOWxaYXZaMi8ySUlZDQpFbE1kSkRCdjd6OFUveElnbFZvaWlSM3p6Yjd4T25ZWnRiS0dNQlVIYThoeVI1ekZaWGRrQ25rRFpPc2gxZzh1DQpReDg9DQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tDQo= + key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ0KTUlJRW93SUJBQUtDQVFFQXFiVGNkUmZ1TWJiY0dBSk0rQ0NGTklTWUo0OGkwQ2FRRFExaENGYW1zYjcrSmYwSQ0KUnJpdFpMK2xvZ3ZrRDhFblJRY2FLRGYySS80OWM2QXViKzNIRGQxMkJrY3U0ZFQ0RDRuV25rNUk0cldqTFEwZw0Kc0hkWk1LdjAxWlRSMThIbkk1Mlo5a0ticG1oUmJ6L1JKK1Z4ampvaWVYNVpOSmVlU3AvYkhiT1N5QVE4WEVKUA0KNTBGeFBUejdKS3BQU3dscTZ0MGFYNUVTL1dqM241c2FBNjJvMGlXTW9tYVVpNEZ4b3lFSWN6QmxtZ3hET0F0Qw0KNlI1ekVuRzJ4TElySUVZUEJSYkFaUjI2dTk3SUZNYjR5SGdhYklKQ1QzclZQNjNiR1puY2UzSWFpRStLNGhkMw0KeStsMHlFVE93MzFjakdrTDBJMTJXOUpjUTYwM2txK1FRVSsrd1FJREFRQUJBb0lCQUQ1c0FqTW56RHVKRVVmYg0KZ01nNzNnTkZTbG95c2hGeVBjWXZSNk96aTdrUmtaWVRqbm5FOERLQXM4SDVNYmdCeWhuLzFNVTZZRlU0N1EyYw0KdTdmNzlCM0xlZUF4U3JOU2pMUGFkWkRoSnJvTkthb21qQUdjeExlOGFHQXZUMGhYVUZldlhyUlFKOFI3MW9oZg0KSnVYUDVZYjFKejBkRmw3YjdpTnd6VDROa1UzMGQ0L3FqUFZGekd0aUp1bGcrK1pSeU4vUURKa3YvR2xVdEdTNA0KVnE4ZVNpdHQyRWY2TE42RFJmN3BKUXE3M1J5bTU2aGtVd2tRVS8vWllwZHo2ZGYzbHZOM0p6cFlBUm80aytCMg0Kend0cHMzdWRtaTRvMHEvVDVNSS96azI5WGVONkc2Z2pDWkZaQU13TlNzNlFWSFdDVDlvbDZPUWJVM1ZmL0ZoTA0KaHV2ZFI4a0NnWUVBMGl3ZU9hN3VYNUJhZmtyQWdKUW8vRVVQVjVLenpiN1M3cVVpNjNMTlVmTHlkVFlrL3B5MQ0KTUwvcHVoN1M0TE9jb05tL0doRFZwSG9RTWs2bHpwSTcyL2hUaTJVRXZ0RHFXR2t0NGhyWDZ2R2NwUERDSWwydg0KcE5EYlNzZkJiZXJ1ME9IbHdRRHl5N1lpaEJ5RHpjOGtzcHJnNjJNa21uc01xd1NyMGF1RldqTUNnWUVBenJYdA0KY3c5Uy9NTlpuZmYzNVo0eklGSVJWenJyM2NYVDJGTWkveG55SlVmVFNIZUNINlNBSUNmT3Zvb01tU0xNY3B1dQ0KRHdDN01Sb3VwT0tPUDZ1Y2ZNMnpobC9WRU9ENXhDdmJNUEw4Y01HaVgxL1dKaVNOTUxmSFZjK3Y5TUduclAvag0KdEZNd0ViL1BBSmR2eVlMRDRKdFVNbGEwTFFrdUVpdHNEMk1CTnpzQ2dZRUF6cUR4Vm1UVmxyNmxPV1RrdllUcw0KaHBpZTdNb2VYRGt2eDlBeTlLaDVsQWYydDZYejJSN1lSSVZwbE1LWk1MRmxXLzY0RXpoWjBzcnZBWWF4SE5aSQ0KWTR0UkY4ckpUNUMxMVJZVE5paU4vejVyY3Y0QTN5aWNkcjJmMjNWb2hsaGpVcG5FK0d1bVNRRllEZXE2NnF5cw0KdDZ2dFZYNnpqZTFNRnhjUWxhSzhDR0VDZ1lBeTg1RDEzWDhkSXFIQ1dFN1lZR2hGdlRUZEJYOENDVE13alQyQg0KRjhvaCtsUlA5blV5aTMreGJWNStoTlNhSW9PMmREMHhJWU1DbFd1TjVQSWZLNVBGQjRjS3hqQmttcSsxOVFGdw0KVFZTQURwcVJXN1FUYnNzR2lTWXZOcVF1ZGxWNFJSTEJiZ2ZaT0NnMEF4L2cwY1NxWmw4WWtWcVVCMEU5NVVvYg0KZU5IaDVRS0JnSFJpQkZaYlc2cHBQYjFWa25jVXVaNXlCUjNkQzhnQzJGZWFqY2toMUtTVFBxeDlYdk9KUXNidQ0KTmx4YUVQbmpxci9LN3Jmd3ExdWRRL0tHNnUxVjNuUWZRUFBGTW85RU5UY3NGbDJoc2dWYk5naGVGV0xsQlA3ZA0KLzRXVDd5cnlja1hkK2FaZ0trWGE2aVE0TFA1ciswM0ppMTd0aG5FcUVDb3hKb1Y5QXg5eg0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0NCg== + ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlETGpDQ0FoYWdBd0lCQWdJV0NBQmhPQkJBZFloM01tbDBGaUlnUlhVVEZvWU9OekFOQmdrcWhraUc5dzBCDQpBUXNGQURBUU1RNHdEQVlEVlFRREV3VnBiMlp2WnpBZUZ3MHlOakEyTWpRd09UTTVNREJhRncweU9UQTJNalF3DQpOVFE0TXpaYU1CQXhEakFNQmdOVkJBTVRCV2x2Wm05bk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBDQpNSUlCQ2dLQ0FRRUFxYlRjZFJmdU1iYmNHQUpNK0NDRk5JU1lKNDhpMENhUURRMWhDRmFtc2I3K0pmMElScml0DQpaTCtsb2d2a0Q4RW5SUWNhS0RmMkkvNDljNkF1YiszSERkMTJCa2N1NGRUNEQ0blduazVJNHJXakxRMGdzSGRaDQpNS3YwMVpUUjE4SG5JNTJaOWtLYnBtaFJiei9SSitWeGpqb2llWDVaTkplZVNwL2JIYk9TeUFROFhFSlA1MEZ4DQpQVHo3SktwUFN3bHE2dDBhWDVFUy9XajNuNXNhQTYybzBpV01vbWFVaTRGeG95RUljekJsbWd4RE9BdEM2UjV6DQpFbkcyeExJcklFWVBCUmJBWlIyNnU5N0lGTWI0eUhnYWJJSkNUM3JWUDYzYkdabmNlM0lhaUUrSzRoZDN5K2wwDQp5RVRPdzMxY2pHa0wwSTEyVzlKY1E2MDNrcStRUVUrK3dRSURBUUFCbzM0d2ZEQU1CZ05WSFJNQkFmOEVBakFBDQpNQTRHQTFVZER3RUIvd1FFQXdJRm9EQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3DQpIZ1lEVlIwUkJCY3dGWWNFd0tocEFvSU5NVGt5TGpFMk9DNHhNRFV1TWpBZEJnTlZIUTRFRmdRVWZTWWJQZnJQDQprZ3NabmlFaUdBeFJGUGdZYlBnd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFZnkzeSt0ZGtnTXVxa3FQZDdpDQoxNWk5elQxeC9ybm5uVjIvRDYxVy9rLzVsZWhSMERRNkxqbWxoNERaMi81b1YxY3JwVHZWUkw5R0dCRUtNdUM1DQo4dzk5NWY3WEQ2eWVabHAwdkJhSlo0OXVHQ1FGMGc5K09kSkNIeHVETU1zaWcxRDk4ZnJYV2tjZkZ4OW9VcW1SDQptQi80VkVCdkt6SVlVREJwZ0lQeWozV24zR1g5WVFtM3J1TjlmNVBvTUsyeHpXVVE3SW5OMjF6WTV4Q2dVajBhDQo1OEhjc002SWdXbW4xU0FaY0h2bGZpaGgvbmtIY3ZDVXA0VzJEeFRNQXFxM2xNSWIwTS9JOWxaYXZaMi8ySUlZDQpFbE1kSkRCdjd6OFUveElnbFZvaWlSM3p6Yjd4T25ZWnRiS0dNQlVIYThoeVI1ekZaWGRrQ25rRFpPc2gxZzh1DQpReDg9DQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tDQo= + + nats: + enabled: true + events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true + controllers: + - name: controlplane + host: 0.0.0.0 + ssh: + user: emirhan + keyFile: /Users/emirhan/.lima/_config/user + port: 62787 + systemAgent: + config: + host: 192.168.105.2 + arch: arm64 + containerEngine: edgelet + deploymentType: native + networkInterface: dynamic + # - name: remote-2 + # host: 192.168.139.148 + # ssh: + # user: alpine + # keyFile: /Users/emirhan/.orbstack/ssh/id_ed25519 + # port: 32222 + # systemAgent: + # config: + # arch: arm64 + # containerEngine: edgelet + # deploymentType: native \ No newline at end of file diff --git a/internal/resource/testdata/remote/lima-vm.yaml b/internal/resource/testdata/remote/lima-vm.yaml new file mode 100644 index 000000000..90e8b7ddb --- /dev/null +++ b/internal/resource/testdata/remote/lima-vm.yaml @@ -0,0 +1,36 @@ +# Lima VM definition for Edgelet embedded-containerd integration tests. +# Ubuntu 24.04 LTS with Apple Virtualization framework (vmType: vz) — +# no QEMU required, boots in ~5s on Apple Silicon. +# https://gist.github.com/yankcrime/4c1b50b7b8dc85757e2bc0baf75d9a82 +# limactl network list +# Usage (from repository root): +# limactl start internal/resource/testdata/remote/lima-vm.yaml --name controlplane + +vmType: vz +os: Linux +arch: aarch64 + +cpus: 2 +memory: "2GiB" +disk: "20GiB" + +images: + - location: "https://cloud-images.ubuntu.com/minimal/releases/noble/release/ubuntu-24.04-minimal-cloudimg-arm64.img" + arch: aarch64 + +rosetta: + enabled: false + +networks: + # - lima: user-v2 + - lima: shared + + +# Prevent Lima's gvproxy from auto-forwarding guest TCP ports to macOS localhost. +# Without this, any port bound inside the VM (e.g. NATS 4222, router 6222) is +# automatically forwarded to the same port on the Mac, which conflicts with +# Docker containers trying to bind those ports when both are active. +portForwards: + - guestPortRange: [1, 65535] + ignore: true + diff --git a/internal/resource/testdata/remote/remote.yaml b/internal/resource/testdata/remote/remote.yaml new file mode 100644 index 000000000..4e2aa7c9d --- /dev/null +++ b/internal/resource/testdata/remote/remote.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: datasance.com/v3 +kind: Agent +metadata: + name: edge-1 +spec: + host: 0.0.0.0 + ssh: + user: emirhan + keyFile: /Users/emirhan/.lima/_config/user + port: 63025 + airgap: true + # package: + # container: + # image: ghcr.io/datasance/edgelet:1.0.0-rc.6 + config: + host: 192.168.105.3 + networkInterface: dynamic + logLevel: INFO + deploymentType: native + containerEngine: edgelet + # containerEngineUrl: unix:///var/run/docker.sock + arch: arm64 \ No newline at end of file diff --git a/internal/resource/trust.go b/internal/resource/trust.go new file mode 100644 index 000000000..a9e443fbf --- /dev/null +++ b/internal/resource/trust.go @@ -0,0 +1,34 @@ +package resource + +import ( + "fmt" +) + +// TrustProvider exposes the CLI-only spec.ca trust certificate (base64 PEM). +// Implemented by all ControlPlane kinds that support spec.ca in reference YAML. +type TrustProvider interface { + GetTrustCA() string +} + +// GetTrustCA returns spec.ca from a control plane when supported, or empty string. +func GetTrustCA(cp ControlPlane) string { + if tp, ok := cp.(TrustProvider); ok { + return tp.GetTrustCA() + } + return "" +} + +// SetTrustCA sets spec.ca on supported control plane kinds. +func SetTrustCA(cp ControlPlane, caBase64 string) error { + switch c := cp.(type) { + case *KubernetesControlPlane: + c.CA = caBase64 + case *LocalControlPlane: + c.CA = caBase64 + case *RemoteControlPlane: + c.CA = caBase64 + default: + return fmt.Errorf("control plane does not support spec.ca") + } + return nil +} diff --git a/internal/resource/trust_test.go b/internal/resource/trust_test.go new file mode 100644 index 000000000..351251867 --- /dev/null +++ b/internal/resource/trust_test.go @@ -0,0 +1,24 @@ +package resource + +import "testing" + +func TestSetTrustCA(t *testing.T) { + ca := "dGVzdC1jYQ==" + + cp := &KubernetesControlPlane{} + if err := SetTrustCA(cp, ca); err != nil { + t.Fatal(err) + } + if cp.CA != ca { + t.Fatalf("CA = %q", cp.CA) + } +} + +func TestSetTrustCA_unsupported(t *testing.T) { + type fakeCP struct { + KubernetesControlPlane + } + if err := SetTrustCA(&fakeCP{}, "x"); err == nil { + t.Fatal("expected error for unsupported control plane type") + } +} diff --git a/internal/resource/types.go b/internal/resource/types.go index 4b6b7fc5e..db6a58409 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -1,22 +1,10 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource import ( "time" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/apps" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/arch" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" ) @@ -30,16 +18,15 @@ type Container struct { } type RemoteContainer struct { - Image string `yaml:"image,omitempty"` - // Repo string `yaml:"repo,omitempty"` - // Credentials Credentials `yaml:"credentials,omitempty"` // Optional credentials if needed to pull images + Image string `yaml:"image,omitempty"` + Registry string `yaml:"registry,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` } type Package struct { - Version string `yaml:"version,omitempty"` - Container RemoteContainer - // Repo string `yaml:"repo,omitempty"` - // Token string `yaml:"token,omitempty"` + Version string `yaml:"version,omitempty"` + Container RemoteContainer `yaml:"container,omitempty"` } type SSH struct { @@ -82,13 +69,52 @@ type Credentials struct { } type Auth struct { - URL string `yaml:"url"` - Realm string `yaml:"realm"` - SSL string `yaml:"ssl"` - RealmKey string `yaml:"realmKey"` - ControllerClient string `yaml:"controllerClient"` - ControllerSecret string `yaml:"controllerSecret"` - ViewerClient string `yaml:"viewerClient"` + Mode string `yaml:"mode"` + InsecureAllowHttp *bool `yaml:"insecureAllowHttp,omitempty"` + InsecureAllowBootstrapLog *bool `yaml:"insecureAllowBootstrapLog,omitempty"` + Bootstrap *AuthBootstrap `yaml:"bootstrap,omitempty"` + IssuerUrl string `yaml:"issuerUrl,omitempty"` + Client *AuthClient `yaml:"client,omitempty"` + ConsoleClient string `yaml:"consoleClient,omitempty"` + ConsoleClientEnabled *bool `yaml:"consoleClientEnabled,omitempty"` + RateLimit *AuthRateLimit `yaml:"rateLimit,omitempty"` + SessionStore *AuthSessionStore `yaml:"sessionStore,omitempty"` + TokenTtl *AuthTokenTtl `yaml:"tokenTtl,omitempty"` + OidcTtl *AuthOidcTtl `yaml:"oidcTtl,omitempty"` +} + +type AuthBootstrap struct { + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` +} + +type AuthClient struct { + ID string `yaml:"id,omitempty"` + Secret string `yaml:"secret,omitempty"` +} + +type AuthRateLimit struct { + Enabled *bool `yaml:"enabled,omitempty"` + MaxRequestsPerWindow int `yaml:"maxRequestsPerWindow,omitempty"` + WindowMs int `yaml:"windowMs,omitempty"` +} + +type AuthSessionStore struct { + Type string `yaml:"type,omitempty"` + TtlMs int `yaml:"ttlMs,omitempty"` + Secret string `yaml:"secret,omitempty"` +} + +type AuthTokenTtl struct { + AccessTokenTtlSeconds int `yaml:"accessTokenTtlSeconds,omitempty"` + RefreshTokenTtlSeconds int `yaml:"refreshTokenTtlSeconds,omitempty"` +} + +type AuthOidcTtl struct { + InteractionTtlSeconds int `yaml:"interactionTtlSeconds,omitempty"` + GrantTtlSeconds int `yaml:"grantTtlSeconds,omitempty"` + SessionTtlSeconds int `yaml:"sessionTtlSeconds,omitempty"` + IdTokenTtlSeconds int `yaml:"idTokenTtlSeconds,omitempty"` } type Database struct { @@ -129,11 +155,13 @@ type Volume struct { } type OfflineImage struct { - Name string `json:"name" yaml:"name"` - X86Image string `json:"x86,omitempty" yaml:"x86,omitempty"` - ArmImage string `json:"arm,omitempty" yaml:"arm,omitempty"` - Auth *OfflineImageAuth `json:"auth,omitempty" yaml:"auth,omitempty"` - Agents []string `json:"agent,omitempty" yaml:"agent,omitempty"` + Name string `json:"name" yaml:"name"` + AMD64Image string `json:"amd64,omitempty" yaml:"amd64,omitempty"` + ARM64Image string `json:"arm64,omitempty" yaml:"arm64,omitempty"` + RISCV64Image string `json:"riscv64,omitempty" yaml:"riscv64,omitempty"` + ArmImage string `json:"arm,omitempty" yaml:"arm,omitempty"` + Auth *OfflineImageAuth `json:"auth,omitempty" yaml:"auth,omitempty"` + Agents []string `json:"agent,omitempty" yaml:"agent,omitempty"` } type OfflineImageAuth struct { @@ -148,7 +176,7 @@ type AgentConfiguration struct { Latitude float64 `json:"latitude,omitempty" yaml:"latitude"` Longitude float64 `json:"longitude,omitempty" yaml:"longitude"` Description string `json:"description,omitempty" yaml:"description"` - FogType *string `json:"fogType,omitempty" yaml:"agentType"` + Arch *string `json:"arch,omitempty" yaml:"arch"` client.AgentConfiguration `yaml:",inline"` } @@ -185,46 +213,35 @@ type AgentStatus struct { IsReadyToRollback bool `json:"isReadyToRollback" yaml:"isReadyToRollback"` Tunnel string `json:"tunnel" yaml:"tunnel"` VolumeMounts []VolumeMount - GpsStatus string `json:"gpsStatus" yaml:"gpsStatus"` -} - -type EdgeResource struct { - Name string - Version string `yaml:"version"` - Description string `yaml:"description"` - InterfaceProtocol string `yaml:"interfaceProtocol"` - Interface *EdgeResourceHTTPInterface `yaml:"interface,omitempty"` // TODO: Make this generic to support multiple interfaces protocols - Display *Display `yaml:"display,omitempty"` - OrchestrationTags []string `yaml:"orchestrationTags"` - Custom map[string]interface{} `yaml:"custom"` + GpsStatus string `json:"gpsStatus" yaml:"gpsStatus"` + AvailableRuntimes []string `json:"availableRuntimes" yaml:"availableRuntimes"` + RuntimeAgentPhase string `json:"runtimeAgentPhase" yaml:"runtimeAgentPhase"` + ControlPlaneQuiesced bool `json:"controlPlaneQuiesced" yaml:"controlPlaneQuiesced"` + PlatformStatus *client.PlatformStatus `json:"platformStatus,omitempty" yaml:"platformStatus,omitempty"` } -type EdgeResourceHTTPInterface = client.HTTPEdgeResource - -type Display = client.EdgeResourceDisplay -type HTTPEndpoint = client.HTTPEndpoint - -// FogTypeStringMap map human readable fog type to Controller fog type -var FogTypeStringMap = map[string]int64{ - "auto": 0, - "x86": 1, - "arm": 2, +// ArchStringToID maps canonical architecture names to Controller archId values. +func ArchStringToID(name string) (int64, bool) { + id, ok := arch.NameToID[name] + return int64(id), ok } -// FogTypeIntMap map Controller fog type to human readable fog type -var FogTypeIntMap = map[int]string{ - 0: "auto", - 1: "x86", - 2: "arm", +// ArchIDToString maps Controller archId values to canonical architecture names. +func ArchIDToString(id int) (string, bool) { + name, ok := arch.IDToName[id] + return name, ok } -type K8SControllerConfig struct { - PidBaseDir string `yaml:"pidBaseDir,omitempty"` - EcnViewerPort int `yaml:"ecnViewerPort,omitempty"` - EcnViewerURL string `yaml:"ecnViewerUrl,omitempty"` - LogLevel string `yaml:"logLevel,omitempty"` - Https *bool `yaml:"https,omitempty"` - SecretName string `yaml:"secretName,omitempty"` +// ControllerConfig is operator-aligned runtime config for the ioFog Controller (spec.controller). +type ControllerConfig struct { + PublicUrl string `yaml:"publicUrl,omitempty"` + TrustProxy *bool `yaml:"trustProxy,omitempty"` + ConsoleUrl string `yaml:"consoleUrl,omitempty"` + ConsolePort int `yaml:"consolePort,omitempty"` + PidBaseDir string `yaml:"pidBaseDir,omitempty"` + LogLevel string `yaml:"logLevel,omitempty"` + Https *bool `yaml:"https,omitempty"` + SecretName string `yaml:"secretName,omitempty"` } type RemoteControllerConfig struct { @@ -331,10 +348,26 @@ type VaultGoogle struct { Credentials string `yaml:"credentials,omitempty"` } -// LocalSystemMicroservices holds optional system image overrides for local control plane. -type LocalSystemMicroservices struct { - Router string `yaml:"router,omitempty"` - Nats string `yaml:"nats,omitempty"` +// LocalControllerSpec is operator-aligned controller config for LocalControlPlane (spec.controller). +type LocalControllerSpec struct { + ControllerConfig `yaml:",inline"` + Package *ControllerPackage `yaml:"package,omitempty"` +} + +// ControllerPackage holds optional controller image and private registry credentials. +type ControllerPackage struct { + Image string `yaml:"image,omitempty"` + Registry string `yaml:"registry,omitempty"` + Email string `yaml:"email,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` +} + +// ControlPlaneTLS holds optional TLS material for non-K8s control planes. +type ControlPlaneTLS struct { + CA string `yaml:"ca,omitempty"` + Cert string `yaml:"cert,omitempty"` + Key string `yaml:"key,omitempty"` } // NatsEnabledConfig is the NATS config for remote and local control planes (enabling only; no service/ingress/jetStream). @@ -491,6 +524,6 @@ type CACreateRequest struct { Name string `json:"name" yaml:"name"` Subject string `json:"subject,omitempty" yaml:"subject,omitempty"` Expiration int `json:"expiration,omitempty" yaml:"expiration,omitempty"` - Type string `json:"type" yaml:"type" yaml:"type"` + Type string `json:"type" yaml:"type"` SecretName string `json:"secretName,omitempty" yaml:"secretName,omitempty"` } diff --git a/internal/resource/user.go b/internal/resource/user.go index e9ae1ff88..e00323c2e 100644 --- a/internal/resource/user.go +++ b/internal/resource/user.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package resource import ( diff --git a/internal/resource/yaml.go b/internal/resource/yaml.go index 5a40d78b4..85651f222 100644 --- a/internal/resource/yaml.go +++ b/internal/resource/yaml.go @@ -25,7 +25,6 @@ func UnmarshallKubernetesControlPlane(file []byte) (controlPlane KubernetesContr } func UnmarshallRemoteControlPlane(file []byte) (controlPlane RemoteControlPlane, err error) { - // Unmarshall the input file if err = yaml.UnmarshalStrict(file, &controlPlane); err != nil { err = util.NewUnmarshalError(err.Error()) return @@ -35,16 +34,13 @@ func UnmarshallRemoteControlPlane(file []byte) (controlPlane RemoteControlPlane, if err = controlPlane.Sanitize(); err != nil { return } - for idx := range controlPlane.Controllers { - if err = controlPlane.Controllers[idx].Sanitize(); err != nil { - return - } + if err = ValidateRemoteControlPlane(&controlPlane); err != nil { + return } return } func UnmarshallLocalControlPlane(file []byte) (controlPlane LocalControlPlane, err error) { - // Unmarshall the input file if err = yaml.UnmarshalStrict(file, &controlPlane); err != nil { err = util.NewUnmarshalError(err.Error()) return @@ -54,7 +50,7 @@ func UnmarshallLocalControlPlane(file []byte) (controlPlane LocalControlPlane, e if err = controlPlane.Sanitize(); err != nil { return } - if err = controlPlane.Controller.Sanitize(); err != nil { + if err = ValidateLocalControlPlane(&controlPlane); err != nil { return } return @@ -103,3 +99,11 @@ func UnmarshallLocalAgent(file []byte) (agent LocalAgent, err error) { err = agent.Sanitize() return } + +func UnmarshallAgentConfiguration(file []byte) (config AgentConfiguration, err error) { + if err = yaml.UnmarshalStrict(file, &config); err != nil { + err = util.NewUnmarshalError(err.Error()) + return + } + return +} diff --git a/internal/rollback/agent.go b/internal/rollback/agent.go index c4b2deadb..16728a554 100644 --- a/internal/rollback/agent.go +++ b/internal/rollback/agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package rollback import ( diff --git a/internal/rollback/factory.go b/internal/rollback/factory.go index f7c74376c..b82b5ecdc 100644 --- a/internal/rollback/factory.go +++ b/internal/rollback/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package rollback import ( diff --git a/internal/start/application/application.go b/internal/start/application/application.go index f921274d6..ad1d313f5 100644 --- a/internal/start/application/application.go +++ b/internal/start/application/application.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package application import ( @@ -45,12 +32,12 @@ func (exe *executor) Execute() (err error) { return err } - flow, err := clt.GetFlowByName(exe.name) + application, err := clt.GetApplicationByName(exe.name) if err != nil { return err } - _, err = clt.StartFlow(flow.ID) + _, err = clt.StartApplication(application.Name) return } diff --git a/internal/start/microservice/microservice.go b/internal/start/microservice/microservice.go index 908278db8..f614ce35f 100644 --- a/internal/start/microservice/microservice.go +++ b/internal/start/microservice/microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package microservice import ( diff --git a/internal/stop/application/application.go b/internal/stop/application/application.go index 65a808b21..490be7ddc 100644 --- a/internal/stop/application/application.go +++ b/internal/stop/application/application.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package application import ( @@ -45,12 +32,12 @@ func (exe *executor) Execute() (err error) { return err } - flow, err := clt.GetFlowByName(exe.name) + application, err := clt.GetApplicationByName(exe.name) if err != nil { return err } - _, err = clt.StopFlow(flow.ID) + _, err = clt.StopApplication(application.Name) return } diff --git a/internal/stop/microservice/microservice.go b/internal/stop/microservice/microservice.go index bdb94e781..9d7e83f11 100644 --- a/internal/stop/microservice/microservice.go +++ b/internal/stop/microservice/microservice.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package microservice import ( diff --git a/internal/trust/ca.go b/internal/trust/ca.go new file mode 100644 index 000000000..614365978 --- /dev/null +++ b/internal/trust/ca.go @@ -0,0 +1,41 @@ +package trust + +import ( + "encoding/base64" + "fmt" + "strings" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +// NormalizeTrustCA validates controller trust material from --ca (PEM file) or --ca-b64. +// Returns base64-encoded PEM suitable for spec.ca storage, or empty when neither input is set. +func NormalizeTrustCA(caFile, caB64 string) (string, error) { + hasFile := strings.TrimSpace(caFile) != "" + hasB64 := strings.TrimSpace(caB64) != "" + if hasFile && hasB64 { + return "", util.NewInputError("Cannot use both --ca and --ca-b64") + } + if !hasFile && !hasB64 { + return "", nil + } + + var pem []byte + var err error + if hasFile { + pem, err = util.ReadUserFile(caFile) + if err != nil { + return "", fmt.Errorf("read CA file: %w", err) + } + } else { + pem, err = base64.StdEncoding.DecodeString(strings.TrimSpace(caB64)) + if err != nil { + return "", fmt.Errorf("decode CA base64: %w", err) + } + } + + if _, err := TransportFromPEM(pem); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(pem), nil +} diff --git a/internal/trust/ca_test.go b/internal/trust/ca_test.go new file mode 100644 index 000000000..4c14b0f9b --- /dev/null +++ b/internal/trust/ca_test.go @@ -0,0 +1,82 @@ +package trust + +import ( + "encoding/base64" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestNormalizeTrustCA_fromFile(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "ca.pem") + writeTestCAPEM(t, path) + pemBytes, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + + got, err := NormalizeTrustCA(path, "") + if err != nil { + t.Fatal(err) + } + want := base64.StdEncoding.EncodeToString(pemBytes) + if got != want { + t.Fatalf("got %q want %q", got, want) + } +} + +func TestNormalizeTrustCA_fromB64(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "ca.pem") + writeTestCAPEM(t, path) + pemBytes, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + encoded := base64.StdEncoding.EncodeToString(pemBytes) + + got, err := NormalizeTrustCA("", encoded) + if err != nil { + t.Fatal(err) + } + if got != encoded { + t.Fatalf("got %q want %q", got, encoded) + } +} + +func TestNormalizeTrustCA_bothFlags(t *testing.T) { + _, err := NormalizeTrustCA("a.pem", "dGVzdA==") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "Cannot use both --ca and --ca-b64") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestNormalizeTrustCA_invalidB64(t *testing.T) { + _, err := NormalizeTrustCA("", "not-valid-base64!!!") + if err == nil { + t.Fatal("expected error") + } +} + +func TestNormalizeTrustCA_invalidPEM(t *testing.T) { + encoded := base64.StdEncoding.EncodeToString([]byte("not a pem")) + _, err := NormalizeTrustCA("", encoded) + if err == nil { + t.Fatal("expected error for invalid PEM content") + } +} + +func TestNormalizeTrustCA_empty(t *testing.T) { + got, err := NormalizeTrustCA("", "") + if err != nil { + t.Fatal(err) + } + if got != "" { + t.Fatalf("expected empty, got %q", got) + } +} diff --git a/internal/trust/probe.go b/internal/trust/probe.go new file mode 100644 index 000000000..3996e650e --- /dev/null +++ b/internal/trust/probe.go @@ -0,0 +1,64 @@ +package trust + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net" + "strings" + "time" +) + +const probeDialTimeout = 10 * time.Second + +// ProbeSystemTrust dials host:port with Go default root CAs and hostname verification. +func ProbeSystemTrust(ctx context.Context, host, port string) (trusted bool, err error) { + if host == "" { + return false, fmt.Errorf("empty TLS probe host") + } + if port == "" { + port = "443" + } + dialer := &net.Dialer{Timeout: probeDialTimeout} + conn, err := tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(host, port), &tls.Config{ + ServerName: host, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: false, + }) + if err != nil { + return false, err + } + _ = conn.Close() + return true, nil +} + +// IsUnknownAuthority reports whether err indicates an untrusted issuing CA. +func IsUnknownAuthority(err error) bool { + if err == nil { + return false + } + var ua x509.UnknownAuthorityError + if errors.As(err, &ua) { + return true + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "unknown authority") || + strings.Contains(msg, "certificate signed by unknown authority") +} + +// IsHostnameMismatch reports whether err indicates SAN / hostname verification failure. +func IsHostnameMismatch(err error) bool { + if err == nil { + return false + } + var hostErr x509.HostnameError + if errors.As(err, &hostErr) { + return true + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "doesn't match") || + strings.Contains(msg, "does not match") || + strings.Contains(msg, "certificate is valid for") +} diff --git a/internal/trust/probe_test.go b/internal/trust/probe_test.go new file mode 100644 index 000000000..8a3020ed5 --- /dev/null +++ b/internal/trust/probe_test.go @@ -0,0 +1,91 @@ +package trust + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "math/big" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestProbeSystemTrust_selfSigned(t *testing.T) { + srv := startHTTPServer(t, "127.0.0.1", []string{"127.0.0.1"}, []net.IP{net.ParseIP("127.0.0.1")}) + defer srv.Close() + + host, port, err := net.SplitHostPort(srv.Listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + + trusted, err := ProbeSystemTrust(context.Background(), host, port) + if trusted { + t.Fatal("expected self-signed cert to fail system trust") + } + if !IsUnknownAuthority(err) { + t.Fatalf("expected unknown authority, got %v", err) + } +} + +func TestIsUnknownAuthorityAndHostnameMismatch(t *testing.T) { + if IsUnknownAuthority(nil) || IsHostnameMismatch(nil) { + t.Fatal("nil should be false") + } + if !IsUnknownAuthority(errors.New("x509: certificate signed by unknown authority")) { + t.Fatal("string match unknown authority") + } + if !IsHostnameMismatch(errors.New(`x509: certificate is valid for example.com, not localhost`)) { + t.Fatal("string match hostname") + } +} + +func startHTTPServer(t *testing.T, cn string, dnsNames []string, ips []net.IP) *httptest.Server { + t.Helper() + tlsCert := selfSignedTLSCert(t, cn, dnsNames, ips) + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + srv.TLS = &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + MinVersion: tls.VersionTLS12, + } + srv.StartTLS() + return srv +} + +func selfSignedTLSCert(t *testing.T, cn string, dnsNames []string, ips []net.IP) tls.Certificate { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + DNSNames: dnsNames, + IPAddresses: ips, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + t.Fatal(err) + } + return tlsCert +} diff --git a/internal/trust/store.go b/internal/trust/store.go new file mode 100644 index 000000000..bf0280f43 --- /dev/null +++ b/internal/trust/store.go @@ -0,0 +1,153 @@ +package trust + +import ( + "encoding/base64" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +var ErrNotFound = errors.New("trust CA not found") + +const ( + caFilename = "ca.pem" + modeFilename = "mode" +) + +// Mode records how TLS trust was established for a CLI namespace. +type Mode string + +const ( + ModeNamespace Mode = "namespace" + ModeSystem Mode = "system" + ModeInsecure Mode = "insecure" +) + +func trustDir(namespace string) (string, error) { + root := config.ConfigFolder() + if root == "" { + return "", fmt.Errorf("config folder is not initialized") + } + return filepath.Join(root, "trust", namespace), nil +} + +func caPath(namespace string) (string, error) { + dir, err := trustDir(namespace) + if err != nil { + return "", err + } + return filepath.Join(dir, caFilename), nil +} + +func modePath(namespace string) (string, error) { + dir, err := trustDir(namespace) + if err != nil { + return "", err + } + return filepath.Join(dir, modeFilename), nil +} + +// StoreCA persists spec.ca (base64 PEM) for a CLI namespace. +func StoreCA(namespace, caBase64 string) error { + if strings.TrimSpace(caBase64) == "" { + return nil + } + pem, err := base64.StdEncoding.DecodeString(caBase64) + if err != nil { + return fmt.Errorf("decode trust CA: %w", err) + } + dir, err := trustDir(namespace) + if err != nil { + return err + } + if err := os.MkdirAll(dir, util.DirPerm); err != nil { + return err + } + path, err := caPath(namespace) + if err != nil { + return err + } + if err := util.WriteValidatedFile(path, pem, util.FilePerm); err != nil { + return err + } + return SetCachedMode(namespace, ModeNamespace) +} + +// GetCA returns the stored PEM for a CLI namespace. +func GetCA(namespace string) ([]byte, error) { + path, err := caPath(namespace) + if err != nil { + return nil, err + } + data, err := util.ReadValidatedFile(path) + if err != nil { + if os.IsNotExist(err) { + return nil, ErrNotFound + } + return nil, err + } + return data, nil +} + +// HasCA reports whether a namespace trust CA file exists. +func HasCA(namespace string) bool { + path, err := caPath(namespace) + if err != nil { + return false + } + _, err = os.Stat(path) + return err == nil +} + +// RemoveCA deletes stored trust material for a namespace (3G delete scope). +func RemoveCA(namespace string) error { + dir, err := trustDir(namespace) + if err != nil { + return err + } + err = os.RemoveAll(dir) + if os.IsNotExist(err) { + return nil + } + return err +} + +// GetCachedMode returns a previously cached TLS trust mode, if any. +func GetCachedMode(namespace string) (Mode, bool) { + path, err := modePath(namespace) + if err != nil { + return "", false + } + data, err := util.ReadValidatedFile(path) + if err != nil { + return "", false + } + mode := Mode(strings.TrimSpace(string(data))) + switch mode { + case ModeSystem, ModeInsecure, ModeNamespace: + return mode, true + default: + return "", false + } +} + +// SetCachedMode persists probe outcome for a namespace. +func SetCachedMode(namespace string, mode Mode) error { + dir, err := trustDir(namespace) + if err != nil { + return err + } + if err := os.MkdirAll(dir, util.DirPerm); err != nil { + return err + } + path, err := modePath(namespace) + if err != nil { + return err + } + return util.WriteValidatedFile(path, []byte(string(mode)), util.FilePerm) +} diff --git a/internal/trust/store_test.go b/internal/trust/store_test.go new file mode 100644 index 000000000..ebcef9f09 --- /dev/null +++ b/internal/trust/store_test.go @@ -0,0 +1,91 @@ +package trust + +import ( + "encoding/base64" + "errors" + "path/filepath" + "testing" + + "github.com/eclipse-iofog/iofogctl/internal/config" +) + +func TestStoreCAAndGetCA(t *testing.T) { + dir := t.TempDir() + config.Init(dir) + + encoded := base64.StdEncoding.EncodeToString([]byte("test-ca-bytes")) + + if err := StoreCA("default", encoded); err != nil { + t.Fatal(err) + } + if !HasCA("default") { + t.Fatal("expected HasCA true") + } + got, err := GetCA("default") + if err != nil { + t.Fatal(err) + } + if string(got) != "test-ca-bytes" { + t.Fatalf("got %q", string(got)) + } + mode, ok := GetCachedMode("default") + if !ok || mode != ModeNamespace { + t.Fatalf("mode = %q ok=%v", mode, ok) + } + + path, _ := caPath("default") + if filepath.Base(path) != "ca.pem" { + t.Fatalf("unexpected ca path %s", path) + } +} + +func TestGetCA_NotFound(t *testing.T) { + dir := t.TempDir() + config.Init(dir) + + _, err := GetCA("missing") + if !errors.Is(err, ErrNotFound) { + t.Fatalf("err = %v", err) + } +} + +func TestSetCachedMode(t *testing.T) { + dir := t.TempDir() + config.Init(dir) + + if err := SetCachedMode("ns1", ModeSystem); err != nil { + t.Fatal(err) + } + mode, ok := GetCachedMode("ns1") + if !ok || mode != ModeSystem { + t.Fatalf("mode = %q ok=%v", mode, ok) + } +} + +func TestRemoveCA(t *testing.T) { + dir := t.TempDir() + config.Init(dir) + + pem := base64.StdEncoding.EncodeToString([]byte("pem")) + if err := StoreCA("rm", pem); err != nil { + t.Fatal(err) + } + if err := RemoveCA("rm"); err != nil { + t.Fatal(err) + } + if HasCA("rm") { + t.Fatal("expected CA removed") + } +} + +func TestStoreCA_emptySkipped(t *testing.T) { + dir := t.TempDir() + config.Init(dir) + + if err := StoreCA("empty", " "); err != nil { + t.Fatal(err) + } + if HasCA("empty") { + t.Fatal("empty CA should not create file") + } +} diff --git a/internal/trust/tls.go b/internal/trust/tls.go new file mode 100644 index 000000000..f41acba44 --- /dev/null +++ b/internal/trust/tls.go @@ -0,0 +1,161 @@ +package trust + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/url" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +// TransportConfig holds TLS settings for controller HTTP calls. +type TransportConfig struct { + SkipVerify bool + TLSConfig *tls.Config +} + +// TransportFromPEM builds a verifying transport from PEM bytes. +func TransportFromPEM(pem []byte) (TransportConfig, error) { + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(pem) { + return TransportConfig{}, fmt.Errorf("failed to parse CA certificate") + } + return TransportConfig{ + TLSConfig: &tls.Config{ + RootCAs: pool, + MinVersion: tls.VersionTLS12, + }, + }, nil +} + +// TransportFromCAFile loads a PEM file for connect-time CA override. +func TransportFromCAFile(caFile string) (TransportConfig, error) { + pem, err := util.ReadUserFile(caFile) + if err != nil { + return TransportConfig{}, fmt.Errorf("read CA file: %w", err) + } + return TransportFromPEM(pem) +} + +// ResolveConnectTransport picks TLS settings for connect using the namespace trust store. +func ResolveConnectTransport(ctx context.Context, namespace, endpoint, caFile string) (TransportConfig, error) { + if strings.TrimSpace(caFile) != "" { + return TransportFromCAFile(caFile) + } + return ResolveTransport(ctx, namespace, endpoint), nil +} + +// ResolveTransport picks TLS settings for an HTTPS controller endpoint. +func ResolveTransport(ctx context.Context, namespace, endpoint string) TransportConfig { + u, err := url.Parse(endpoint) + if err != nil || u.Scheme != "https" { + return TransportConfig{TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}} + } + + if HasCA(namespace) { + if pem, err := GetCA(namespace); err == nil { + pool := x509.NewCertPool() + if pool.AppendCertsFromPEM(pem) { + return TransportConfig{ + TLSConfig: &tls.Config{ + RootCAs: pool, + MinVersion: tls.VersionTLS12, + }, + } + } + } + } + + if mode, ok := GetCachedMode(namespace); ok { + switch mode { + case ModeSystem: + return TransportConfig{ + TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}, + } + case ModeInsecure: + return TransportConfig{ + SkipVerify: true, + TLSConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}, // #nosec G402 + } + } + } + + host, port := hostPort(u) + trusted, probeErr := ProbeSystemTrust(ctx, host, port) + switch { + case trusted: + _ = SetCachedMode(namespace, ModeSystem) + return TransportConfig{ + TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}, + } + case IsUnknownAuthority(probeErr): + util.PrintWarning(insecureTLSWarning(namespace, + `Controller TLS certificate is not trusted by the system CA store for namespace "%s". Certificate verification is disabled.`, + )) + _ = SetCachedMode(namespace, ModeInsecure) + return TransportConfig{ + SkipVerify: true, + TLSConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}, // #nosec G402 + } + case IsHostnameMismatch(probeErr): + util.PrintWarning(fmt.Sprintf( + `Controller endpoint host "%s" does not match the TLS certificate. Check publicUrl or ingress host in your Control Plane YAML.`, + host, + )) + return TransportConfig{ + SkipVerify: true, + TLSConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}, // #nosec G402 + } + default: + util.PrintWarning(insecureTLSWarning(namespace, + `Controller TLS probe failed for namespace "%s". Certificate verification is disabled.`, + )) + return TransportConfig{ + SkipVerify: true, + TLSConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}, // #nosec G402 + } + } +} + +// TLSConfigForController resolves TLS settings for controller HTTP/WSS calls in a namespace. +func TLSConfigForController(ctx context.Context, namespace, controllerURL string) *tls.Config { + return ResolveTransport(ctx, namespace, controllerURL).TLSConfig +} + +// SDKOptions builds SDK client options with namespace-aware TLS for controller API calls. +func SDKOptions(ctx context.Context, namespace, endpoint string) (client.Options, error) { + baseURL, err := util.GetBaseURL(endpoint) + if err != nil { + return client.Options{}, err + } + tlsCfg := TLSConfigForController(ctx, namespace, endpoint) + return client.Options{ + BaseURL: baseURL, + TLSConfig: tlsCfg, + }, nil +} + +func insecureTLSWarning(namespace, msg string) string { + warning := fmt.Sprintf(msg, namespace) + if !HasCA(namespace) { + warning += ` Provide spec.ca in your Control Plane YAML, or pass connect/configure --ca or --ca-b64, to enable verification.` + } + return warning +} + +func hostPort(u *url.URL) (host, port string) { + host = u.Hostname() + port = u.Port() + if port == "" { + if u.Scheme == "https" { + port = "443" + } else { + port = "80" + } + } + return host, port +} diff --git a/internal/trust/tls_connect_test.go b/internal/trust/tls_connect_test.go new file mode 100644 index 000000000..e4bd47e0f --- /dev/null +++ b/internal/trust/tls_connect_test.go @@ -0,0 +1,67 @@ +package trust + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" +) + +func writeTestCAPEM(t *testing.T, path string) { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test-ca"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + } + der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + if err := pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil { + t.Fatal(err) + } +} + +func TestResolveConnectTransportCAOverride(t *testing.T) { + dir := t.TempDir() + caPath := filepath.Join(dir, "ca.pem") + writeTestCAPEM(t, caPath) + + cfg, err := ResolveConnectTransport(context.Background(), "ns", "https://controller.example.com", caPath) + if err != nil { + t.Fatalf("ResolveConnectTransport: %v", err) + } + if cfg.TLSConfig == nil || cfg.TLSConfig.RootCAs == nil { + t.Fatal("expected verifying TLS config from CA file override") + } + if cfg.SkipVerify { + t.Fatal("CA override should not skip verify") + } +} + +func TestTransportFromCAFileMissing(t *testing.T) { + _, err := TransportFromCAFile(filepath.Join(t.TempDir(), "missing.pem")) + if err == nil { + t.Fatal("expected error for missing CA file") + } +} diff --git a/internal/trust/wait.go b/internal/trust/wait.go new file mode 100644 index 000000000..763b7869c --- /dev/null +++ b/internal/trust/wait.go @@ -0,0 +1,53 @@ +package trust + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const controllerAPIWaitSeconds = 60 + +// WaitForControllerAPI polls the controller /status endpoint with trust-aware TLS. +func WaitForControllerAPI(ctx context.Context, namespace, endpoint string) error { + baseURL, err := util.GetBaseURL(endpoint) + if err != nil { + return err + } + statusURL := baseURL.String() + "/status" + + var lastErr error + for seconds := 0; seconds < controllerAPIWaitSeconds; seconds++ { + cfg := ResolveTransport(ctx, namespace, endpoint) + client := &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: cfg.TLSConfig, + }, + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, statusURL, nil) + if err != nil { + return err + } + resp, err := client.Do(req) + if err == nil { + util.DrainAndCloseHTTPBody(resp.Body) + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return nil + } + lastErr = fmt.Errorf("controller status returned %d", resp.StatusCode) + } else { + lastErr = err + } + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(time.Second): + } + } + return lastErr +} diff --git a/internal/upgrade/agent.go b/internal/upgrade/agent.go index 8a4465aa6..3999df120 100644 --- a/internal/upgrade/agent.go +++ b/internal/upgrade/agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package upgrade import ( diff --git a/internal/upgrade/factory.go b/internal/upgrade/factory.go index b8fe134f6..b5b67f8e4 100644 --- a/internal/upgrade/factory.go +++ b/internal/upgrade/factory.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package upgrade import ( diff --git a/internal/util/client/api.go b/internal/util/client/api.go index e36748cc0..83e5b7678 100644 --- a/internal/util/client/api.go +++ b/internal/util/client/api.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package client import ( @@ -29,6 +16,13 @@ func InvalidateCache() { pkg.agentCacheRequestChan <- newAgentCacheRequest("") } +// InvalidateAgentCache clears cached agents for a namespace. +func InvalidateAgentCache(namespace string) { + request := newAgentCacheInvalidateRequest(namespace) + pkg.agentCacheRequestChan <- request + <-request.resultChan +} + // NewControllerClient will return cached client or create new client and cache it func NewControllerClient(namespace string) (*client.Client, error) { request := newClientCacheRequest(namespace) @@ -52,18 +46,6 @@ func SyncAgentInfo(namespace string) error { return <-request.resultChan } -func IsEdgeResourceCapable(namespace string) error { - // Check Controller API handles edge resources - clt, err := NewControllerClient(namespace) - if err != nil { - return err - } - if err := clt.IsEdgeResourceCapable(); err != nil { - return err - } - return nil -} - func GetMicroserviceName(namespace, appName, msvcName string) (name string, err error) { clt, err := NewControllerClient(namespace) if err != nil { @@ -178,9 +160,9 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura agentMapByUUID[agent.UUID] = *agent } - fogType, found := rsc.FogTypeIntMap[agentInfo.FogType] + arch, found := rsc.ArchIDToString(agentInfo.ArchID) if !found { - fogType = "auto" + arch = "auto" } routerConfig := client.RouterConfig{ @@ -206,7 +188,7 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura NatsLeafPort: agentInfo.NatsLeafPort, NatsClusterPort: agentInfo.NatsClusterPort, NatsMqttPort: agentInfo.NatsMqttPort, - NatsHttpPort: agentInfo.NatsHttpPort, + NatsHTTPPort: agentInfo.NatsHTTPPort, JsStorageSize: agentInfo.JsStorageSize, JsMemoryStoreSize: agentInfo.JsMemoryStoreSize, } @@ -232,10 +214,10 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura Latitude: agentInfo.Latitude, Longitude: agentInfo.Longitude, Description: agentInfo.Description, - FogType: &fogType, + Arch: &arch, AgentConfiguration: client.AgentConfiguration{ NetworkInterface: &agentInfo.NetworkInterface, - DockerURL: &agentInfo.DockerURL, + ContainerEngineURL: &agentInfo.ContainerEngineURL, ContainerEngine: &agentInfo.ContainerEngine, DeploymentType: &agentInfo.DeploymentType, DiskLimit: &agentInfo.DiskLimit, @@ -256,7 +238,7 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura EdgeGuardFrequency: &agentInfo.EdgeGuardFrequency, AbstractedHardwareEnabled: &agentInfo.AbstractedHardwareEnabled, LogLevel: agentInfo.LogLevel, - DockerPruningFrequency: agentInfo.DockerPruningFrequency, + PruningFrequency: agentInfo.PruningFrequency, AvailableDiskThreshold: agentInfo.AvailableDiskThreshold, UpstreamRouters: upstreamRoutersPtr, NetworkRouter: networkRouterPtr, @@ -287,8 +269,6 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura LastStatusTimeMsUTC: agentInfo.LastStatusTimeMsUTC, IPAddress: agentInfo.IPAddress, IPAddressExternal: agentInfo.IPAddressExternal, - ProcessedMessaged: agentInfo.ProcessedMessaged, - MessageSpeed: agentInfo.MessageSpeed, LastCommandTimeMsUTC: agentInfo.LastCommandTimeMsUTC, Version: agentInfo.Version, IsReadyToUpgrade: agentInfo.IsReadyToUpgrade, @@ -296,6 +276,10 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura Tunnel: agentInfo.Tunnel, VolumeMounts: convertVolumeMounts(agentInfo.VolumeMounts), GpsStatus: agentInfo.GpsStatus, + AvailableRuntimes: []string(agentInfo.AvailableRuntimes), + RuntimeAgentPhase: agentInfo.RuntimeAgentPhase, + ControlPlaneQuiesced: agentInfo.ControlPlaneQuiesced, + PlatformStatus: agentInfo.PlatformStatus, } return agentConfig, tags, agentStatus, err diff --git a/internal/util/client/client.go b/internal/util/client/client.go index edd88d20d..0755a039e 100644 --- a/internal/util/client/client.go +++ b/internal/util/client/client.go @@ -1,29 +1,23 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package client import ( + "context" "fmt" "strings" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/config" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/internal/trust" "github.com/eclipse-iofog/iofogctl/pkg/iofog" "github.com/eclipse-iofog/iofogctl/pkg/util" ) +// ControllerClientOptions builds SDK client options with namespace-aware TLS for controller API calls. +func ControllerClientOptions(ctx context.Context, namespace, endpoint string) (client.Options, error) { + return trust.SDKOptions(ctx, namespace, endpoint) +} + // clientCacheRoutine handles concurrent requests for a cached Controller client func clientCacheRoutine() { for { @@ -62,6 +56,12 @@ func agentCacheRoutine() { if request.namespace == "" { // Invalidate cache pkg.agentCache = make(map[string][]client.AgentInfo) + request.resultChan <- &agentCacheResult{} + continue + } + if request.invalidate { + delete(pkg.agentCache, request.namespace) + request.resultChan <- &agentCacheResult{} continue } result := &agentCacheResult{} @@ -93,18 +93,12 @@ func agentCacheRoutine() { } func agentSyncRoutine() { - complete := false for { request := <-pkg.agentSyncRequestChan - if complete { - request.resultChan <- nil - continue - } if err := syncAgentInfo(request.namespace); err != nil { request.resultChan <- err continue } - complete = true request.resultChan <- nil } } @@ -120,9 +114,8 @@ func syncAgentInfo(namespace string) error { if err != nil { return err } - if _, ok := controlPlane.(*rsc.LocalControlPlane); ok { - // Do not update local Agents - return nil + if localCP, ok := controlPlane.(*rsc.LocalControlPlane); ok { + return syncLocalControlPlaneAgents(ns, localCP, namespace) } // Generate map of config Agents agentsMap := make(map[string]*rsc.RemoteAgent) @@ -150,18 +143,7 @@ func syncAgentInfo(namespace string) error { continue } - agent := rsc.RemoteAgent{ - Name: backendAgent.Name, - UUID: backendAgent.UUID, - Host: backendAgent.Host, - } - // Update additional info if local cache contains it - if cachedAgent, exists := agentsMap[backendAgent.Name]; exists { - agent.Created = cachedAgent.GetCreatedTime() - agent.SSH = cachedAgent.SSH - } - - agents[idx] = agent + agents[idx] = mergeRemoteAgentFromBackend(agentsMap[backendAgent.Name], backendAgent) } // Overwrite the Agents @@ -181,6 +163,110 @@ func syncAgentInfo(namespace string) error { return config.Flush() } +func syncLocalControlPlaneAgents(ns *rsc.Namespace, cp *rsc.LocalControlPlane, namespace string) error { + localAgentsMap := make(map[string]*rsc.LocalAgent) + remoteAgentsMap := make(map[string]*rsc.RemoteAgent) + for _, baseAgent := range ns.GetAgents() { + switch agent := baseAgent.(type) { + case *rsc.LocalAgent: + localAgentsMap[agent.GetName()] = agent.Clone().(*rsc.LocalAgent) + case *rsc.RemoteAgent: + remoteAgentsMap[agent.GetName()] = agent.Clone().(*rsc.RemoteAgent) + } + } + + backendAgents, err := GetBackendAgents(namespace) + if err != nil { + return err + } + + endpoint, _ := cp.GetEndpoint() + + ns.DeleteAgents() + for idx := range backendAgents { + backendAgent := &backendAgents[idx] + if backendAgent.IsSystem { + localAgent := mergeLocalAgentFromBackend(localAgentsMap[backendAgent.Name], backendAgent, cp) + if err := ns.AddAgent(&localAgent); err != nil { + return err + } + continue + } + + remoteAgent := mergeRemoteAgentFromBackend(remoteAgentsMap[backendAgent.Name], backendAgent) + remoteAgent.ControllerEndpoint = endpoint + remoteAgent.Airgap = cp.Airgap + if err := ns.AddAgent(&remoteAgent); err != nil { + return err + } + } + return config.Flush() +} + +func mergeLocalAgentFromBackend(cached *rsc.LocalAgent, backend *client.AgentInfo, cp *rsc.LocalControlPlane) rsc.LocalAgent { + var agent rsc.LocalAgent + if cached != nil { + agent = *cached.Clone().(*rsc.LocalAgent) + } else { + agent = rsc.LocalAgent{ + Name: backend.Name, + Host: backend.Host, + } + if backend.IsSystem && cp.SystemAgent != nil { + agent.Package = cp.SystemAgent.Package + agent.Scripts = cp.SystemAgent.Scripts + if cp.SystemAgent.AgentConfiguration != nil { + cfg := *cp.SystemAgent.AgentConfiguration + agent.Config = &cfg + } + } + } + + agent.Name = backend.Name + agent.UUID = backend.UUID + if agent.Host == "" { + agent.Host = backend.Host + } + if endpoint, err := cp.GetEndpoint(); err == nil { + agent.ControllerEndpoint = endpoint + } + agent.Airgap = cp.Airgap + return agent +} + +// mergeRemoteAgentFromBackend updates UUID and Controller registration host from the API +// while preserving locally configured SSH host (spec.host) and deploy metadata. +func mergeRemoteAgentFromBackend(cached *rsc.RemoteAgent, backend *client.AgentInfo) rsc.RemoteAgent { + var agent rsc.RemoteAgent + if cached != nil { + agent = *cached.Clone().(*rsc.RemoteAgent) + } else { + agent = rsc.RemoteAgent{ + Name: backend.Name, + Host: backend.Host, + } + } + + agent.Name = backend.Name + agent.UUID = backend.UUID + if agent.Host == "" { + agent.Host = backend.Host + } + setRemoteAgentRegistrationHost(&agent, backend.Host) + return agent +} + +func setRemoteAgentRegistrationHost(agent *rsc.RemoteAgent, registrationHost string) { + if registrationHost == "" { + return + } + if agent.Config == nil { + agent.Config = &rsc.AgentConfiguration{} + } + host := registrationHost + agent.Config.Host = &host +} + func newControllerClient(namespace string) (*client.Client, error) { // Try to get the client from the cache first @@ -204,30 +290,31 @@ func newControllerClient(namespace string) (*client.Client, error) { user := controlPlane.GetUser() - // Get base URL - baseURL, err := util.GetBaseURL(endpoint) - if err != nil { - return nil, err - } - // Use the refresh token from the cached client refreshToken := cachedClient.GetRefreshToken() user.AccessToken = cachedClient.GetAccessToken() user.RefreshToken = cachedClient.GetRefreshToken() // controlPlane.UpdateUserTokens(user.AccessToken, user.RefreshToken) - config.UpdateUser(namespace, user.AccessToken, user.RefreshToken) + _ = config.UpdateUser(namespace, user.AccessToken, user.RefreshToken) + + opt, err := ControllerClientOptions(context.Background(), namespace, endpoint) + if err != nil { + return nil, err + } // Use SessionLogin to attempt to refresh the session util.SpinHandlePrompt() - refreshedClient, err := client.SessionLogin(client.Options{BaseURL: baseURL}, refreshToken, user.Email, user.GetRawPassword()) + refreshedClient, err := client.SessionLogin(opt, refreshToken, user.Email, user.GetRawPassword()) if err != nil { fmt.Println("Error: Failed to refresh session:", err) - return nil, fmt.Errorf("failed to refresh session: %v", err) + return nil, fmt.Errorf("failed to refresh session: %w", err) } util.SpinHandlePromptComplete() // Update the cached client with the refreshed session pkg.clientCache[namespace] = refreshedClient - config.Flush() + if err := config.Flush(); err != nil { + return nil, fmt.Errorf("failed to persist namespace after session refresh: %w", err) + } return refreshedClient, nil } @@ -246,14 +333,15 @@ func newControllerClient(namespace string) (*client.Client, error) { } user := controlPlane.GetUser() - baseURL, err := util.GetBaseURL(endpoint) + + opt, err := ControllerClientOptions(context.Background(), namespace, endpoint) if err != nil { return nil, err } // Create a new client and login util.SpinHandlePrompt() - newClient, err := client.SessionLogin(client.Options{BaseURL: baseURL}, user.RefreshToken, user.Email, user.GetRawPassword()) + newClient, err := client.SessionLogin(opt, user.RefreshToken, user.Email, user.GetRawPassword()) if err != nil { return nil, err } @@ -261,11 +349,11 @@ func newControllerClient(namespace string) (*client.Client, error) { user.AccessToken = newClient.GetAccessToken() user.RefreshToken = newClient.GetRefreshToken() // controlPlane.UpdateUserTokens(user.AccessToken, user.RefreshToken) - config.UpdateUser(namespace, user.AccessToken, user.RefreshToken) + _ = config.UpdateUser(namespace, user.AccessToken, user.RefreshToken) // Flush the config and handle errors if err := config.Flush(); err != nil { - return nil, fmt.Errorf("failed to flush config: %v", err) + return nil, fmt.Errorf("failed to flush config: %w", err) } return newClient, nil @@ -290,7 +378,7 @@ func getBackendAgents(namespace string, ioClient *client.Client) ([]client.Agent // Refresh authentication and retry refreshedClient, refreshErr := refreshClientAuthentication(namespace) if refreshErr != nil { - return nil, fmt.Errorf("authentication error occurred and failed to refresh: %v (refresh error: %v)", err, refreshErr) + return nil, fmt.Errorf("authentication error occurred and failed to refresh: %w (refresh error: %w)", err, refreshErr) } // Retry the operation with refreshed client @@ -378,31 +466,32 @@ func refreshClientAuthentication(namespace string) (*client.Client, error) { } user := controlPlane.GetUser() - baseURL, err := util.GetBaseURL(endpoint) + + opt, err := ControllerClientOptions(context.Background(), namespace, endpoint) if err != nil { return nil, err } // Re-authenticate using SessionLogin util.SpinHandlePrompt() - refreshedClient, err := client.SessionLogin(client.Options{BaseURL: baseURL}, user.RefreshToken, user.Email, user.GetRawPassword()) + refreshedClient, err := client.SessionLogin(opt, user.RefreshToken, user.Email, user.GetRawPassword()) if err != nil { util.SpinHandlePromptComplete() - return nil, fmt.Errorf("failed to refresh authentication: %v", err) + return nil, fmt.Errorf("failed to refresh authentication: %w", err) } util.SpinHandlePromptComplete() // Update tokens in config user.AccessToken = refreshedClient.GetAccessToken() user.RefreshToken = refreshedClient.GetRefreshToken() - config.UpdateUser(namespace, user.AccessToken, user.RefreshToken) + _ = config.UpdateUser(namespace, user.AccessToken, user.RefreshToken) // Update cached client pkg.clientCache[namespace] = refreshedClient // Flush config if err := config.Flush(); err != nil { - return nil, fmt.Errorf("failed to flush config: %v", err) + return nil, fmt.Errorf("failed to flush config: %w", err) } return refreshedClient, nil @@ -430,7 +519,7 @@ func ExecuteWithAuthRetry(namespace string, operation func(*client.Client) error // Refresh authentication and retry refreshedClient, refreshErr := refreshClientAuthentication(namespace) if refreshErr != nil { - return fmt.Errorf("authentication error occurred and failed to refresh: %v (refresh error: %v)", err, refreshErr) + return fmt.Errorf("authentication error occurred and failed to refresh: %w (refresh error: %w)", err, refreshErr) } // Retry the operation with refreshed client diff --git a/internal/util/client/client_local_cp_sync_test.go b/internal/util/client/client_local_cp_sync_test.go new file mode 100644 index 000000000..f4f9c7a91 --- /dev/null +++ b/internal/util/client/client_local_cp_sync_test.go @@ -0,0 +1,70 @@ +package client + +import ( + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" +) + +func TestMergeLocalAgentFromBackendOnlySeedsSystemAgentMetadataForSystemAgents(t *testing.T) { + t.Parallel() + + cp := &rsc.LocalControlPlane{ + SystemAgent: &rsc.SystemAgentConfig{ + Package: rsc.Package{Version: "system-pkg"}, + }, + } + + nonSystem := mergeLocalAgentFromBackend(nil, &client.AgentInfo{ + Name: "edge-3", + UUID: "uuid-edge-3", + Host: "192.168.139.27", + IsSystem: false, + }, cp) + if nonSystem.Package.Version == "system-pkg" { + t.Fatalf("non-system agent inherited CP system package: %+v", nonSystem.Package) + } + + system := mergeLocalAgentFromBackend(nil, &client.AgentInfo{ + Name: "iofog", + UUID: "uuid-iofog", + Host: "192.168.1.6", + IsSystem: true, + }, cp) + if system.Package.Version != "system-pkg" { + t.Fatalf("system agent package = %+v, want system-pkg", system.Package) + } +} + +func TestMergeRemoteAgentFromBackendPreservesSSHHostForLocalCPAgents(t *testing.T) { + t.Parallel() + + sshHost := "0.0.0.0" + registrationHost := "192.168.139.27" + cached := &rsc.RemoteAgent{ + Name: "edge-3", + Host: sshHost, + SSH: rsc.SSH{User: "ubuntu", Port: 22, KeyFile: "/tmp/id_ed25519"}, + Package: rsc.Package{ + Version: "v1.0.0-rc.6", + }, + } + + merged := mergeRemoteAgentFromBackend(cached, &client.AgentInfo{ + Name: "edge-3", + UUID: "uuid-edge-3", + Host: registrationHost, + IsSystem: false, + }) + + if merged.Host != sshHost { + t.Fatalf("SSH host = %q, want %q", merged.Host, sshHost) + } + if merged.SSH.User != "ubuntu" { + t.Fatalf("SSH user = %q", merged.SSH.User) + } + if merged.Config == nil || merged.Config.Host == nil || *merged.Config.Host != registrationHost { + t.Fatalf("registration host = %v, want %q", merged.Config, registrationHost) + } +} diff --git a/internal/util/client/client_sync_test.go b/internal/util/client/client_sync_test.go new file mode 100644 index 000000000..55008cb0c --- /dev/null +++ b/internal/util/client/client_sync_test.go @@ -0,0 +1,60 @@ +package client + +import ( + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" +) + +func TestMergeRemoteAgentFromBackendPreservesSSHHost(t *testing.T) { + t.Parallel() + + registrationHost := "192.168.139.184" + sshHost := "0.0.0.0" + cached := &rsc.RemoteAgent{ + Name: "edge-1", + Host: sshHost, + SSH: rsc.SSH{User: "ubuntu2", Port: 32222, KeyFile: "/tmp/id_ed25519"}, + Airgap: true, + Package: rsc.Package{ + Version: "v1.0.0-rc.6", + }, + } + + merged := mergeRemoteAgentFromBackend(cached, &client.AgentInfo{ + Name: "edge-1", + UUID: "055bcc8d-91d2-445e-b7a2-f48e5bd98046", + Host: registrationHost, + }) + + if merged.Host != sshHost { + t.Fatalf("SSH host = %q, want %q", merged.Host, sshHost) + } + if merged.Config == nil || merged.Config.Host == nil || *merged.Config.Host != registrationHost { + t.Fatalf("registration host = %v, want %q", merged.Config, registrationHost) + } + if merged.SSH.User != "ubuntu2" || !merged.Airgap || merged.Package.Version != "v1.0.0-rc.6" { + t.Fatalf("cached deploy metadata lost: %+v", merged) + } + if merged.UUID != "055bcc8d-91d2-445e-b7a2-f48e5bd98046" { + t.Fatalf("UUID = %q", merged.UUID) + } +} + +func TestMergeRemoteAgentFromBackendWithoutCacheUsesBackendHost(t *testing.T) { + t.Parallel() + + merged := mergeRemoteAgentFromBackend(nil, &client.AgentInfo{ + Name: "edge-1", + UUID: "uuid-1", + Host: "192.168.139.184", + }) + + if merged.Host != "192.168.139.184" { + t.Fatalf("Host = %q", merged.Host) + } + if merged.Config == nil || merged.Config.Host == nil || *merged.Config.Host != "192.168.139.184" { + t.Fatalf("registration host = %v", merged.Config) + } +} diff --git a/internal/util/client/pkg.go b/internal/util/client/pkg.go index 9594d8e8e..7f1cda994 100644 --- a/internal/util/client/pkg.go +++ b/internal/util/client/pkg.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package client import ( @@ -63,6 +50,7 @@ func (ccr *clientCacheResult) get() (*client.Client, error) { type agentCacheRequest struct { namespace string + invalidate bool resultChan chan *agentCacheResult } @@ -73,6 +61,14 @@ func newAgentCacheRequest(namespace string) *agentCacheRequest { } } +func newAgentCacheInvalidateRequest(namespace string) *agentCacheRequest { + return &agentCacheRequest{ + namespace: namespace, + invalidate: true, + resultChan: make(chan *agentCacheResult), + } +} + type agentCacheResult struct { err error agents []client.AgentInfo diff --git a/internal/util/client/platform.go b/internal/util/client/platform.go new file mode 100644 index 000000000..0fc74a0eb --- /dev/null +++ b/internal/util/client/platform.go @@ -0,0 +1,105 @@ +package client + +import ( + "strings" + "time" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" +) + +// PlatformWaitTimeout matches test/func/wait.bash microservice wait budget (20 × 20s). +const PlatformWaitTimeout = 400 * time.Second + +func isAgentPlatformFailed(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "platform reconcile failed") +} + +func isServiceProvisioningFailed(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "provisioning failed") +} + +// WaitForAgentPlatformReadyWithRetry waits for fog platform Ready; on Failed, ReconcileAgent once and re-wait. +func WaitForAgentPlatformReadyWithRetry(namespace, uuid string) error { + return ExecuteWithAuthRetry(namespace, func(clt *client.Client) error { + err := clt.WaitForAgentPlatformReady(uuid, PlatformWaitTimeout) + if err == nil { + return nil + } + if !isAgentPlatformFailed(err) { + return err + } + if _, recErr := clt.ReconcileAgent(uuid); recErr != nil { + return err + } + return clt.WaitForAgentPlatformReady(uuid, PlatformWaitTimeout) + }) +} + +// WaitForServiceProvisioningReadyWithRetry waits for hub provisioning ready; on Failed, ReconcileService once and re-wait. +func WaitForServiceProvisioningReadyWithRetry(namespace, name string) error { + return ExecuteWithAuthRetry(namespace, func(clt *client.Client) error { + err := clt.WaitForServiceProvisioningReady(name, PlatformWaitTimeout) + if err == nil { + return nil + } + if !isServiceProvisioningFailed(err) { + return err + } + if _, recErr := clt.ReconcileService(name); recErr != nil { + return err + } + return clt.WaitForServiceProvisioningReady(name, PlatformWaitTimeout) + }) +} + +// ReconcileAgentByName resolves an agent name and enqueues platform reconcile. +func ReconcileAgentByName(namespace, name string) error { + return ExecuteWithAuthRetry(namespace, func(clt *client.Client) error { + agent, err := clt.GetAgentByName(name) + if err != nil { + return err + } + _, err = clt.ReconcileAgent(agent.UUID) + return err + }) +} + +// ReconcileAgentByNameAndWait reconciles an agent platform and waits until Ready. +func ReconcileAgentByNameAndWait(namespace, name string) error { + var uuid string + err := ExecuteWithAuthRetry(namespace, func(clt *client.Client) error { + agent, err := clt.GetAgentByName(name) + if err != nil { + return err + } + uuid = agent.UUID + _, err = clt.ReconcileAgent(uuid) + return err + }) + if err != nil { + return err + } + return WaitForAgentPlatformReadyWithRetry(namespace, uuid) +} + +// ReconcileServiceByName enqueues service hub reconcile. +func ReconcileServiceByName(namespace, name string) error { + return ExecuteWithAuthRetry(namespace, func(clt *client.Client) error { + _, err := clt.ReconcileService(name) + return err + }) +} + +// ReconcileServiceByNameAndWait reconciles a service hub and waits until ready. +func ReconcileServiceByNameAndWait(namespace, name string) error { + if err := ReconcileServiceByName(namespace, name); err != nil { + return err + } + return WaitForServiceProvisioningReadyWithRetry(namespace, name) +} diff --git a/internal/util/client/platform_test.go b/internal/util/client/platform_test.go new file mode 100644 index 000000000..87a10ca0b --- /dev/null +++ b/internal/util/client/platform_test.go @@ -0,0 +1,34 @@ +package client + +import ( + "errors" + "testing" + "time" +) + +func TestIsAgentPlatformFailed(t *testing.T) { + err := errors.New("agent abc platform reconcile failed: router timeout") + if !isAgentPlatformFailed(err) { + t.Fatal("expected platform failed detection") + } + if isAgentPlatformFailed(errors.New("timed out waiting for agent abc platform ready (phase=Progressing)")) { + t.Fatal("timeout should not be treated as failed") + } +} + +func TestIsServiceProvisioningFailed(t *testing.T) { + err := errors.New("service foo provisioning failed: hub error") + if !isServiceProvisioningFailed(err) { + t.Fatal("expected provisioning failed detection") + } + if isServiceProvisioningFailed(errors.New("timed out waiting for service foo provisioning ready (status=pending)")) { + t.Fatal("timeout should not be treated as failed") + } +} + +func TestPlatformWaitTimeoutMatchesTests(t *testing.T) { + const wantSeconds = 400 + if PlatformWaitTimeout != time.Duration(wantSeconds)*time.Second { + t.Fatalf("PlatformWaitTimeout = %v, want %ds", PlatformWaitTimeout, wantSeconds) + } +} diff --git a/internal/util/misc.go b/internal/util/misc.go index 836448df5..0af591a46 100644 --- a/internal/util/misc.go +++ b/internal/util/misc.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( diff --git a/internal/util/terminal/terminal.go b/internal/util/terminal/terminal.go deleted file mode 100644 index d2808b638..000000000 --- a/internal/util/terminal/terminal.go +++ /dev/null @@ -1,357 +0,0 @@ -//go:build !windows -// +build !windows - -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package terminal - -import ( - "context" - "fmt" - "os" - "os/signal" - "sync" - "time" - - ws "github.com/eclipse-iofog/iofogctl/internal/util/websocket" - "golang.org/x/sys/unix" - "golang.org/x/term" -) - -type Terminal struct { - wsClient *ws.Client - oldState *term.State - history []string - histIdx int - inputBuffer []rune - cursorPos int - resizeCh chan os.Signal - // lastCommand string - lastCtrlCTime time.Time - prompt string - stdoutMutex sync.Mutex - ctx context.Context - cancel context.CancelFunc - cleanupOnce sync.Once - // isEditorMode bool -} - -// var promptPattern = regexp.MustCompile(`(?m)^.*[@].*[$#] ?$`) - -func NewTerminal(wsClient *ws.Client) *Terminal { - ctx, cancel := context.WithCancel(context.Background()) - return &Terminal{ - wsClient: wsClient, - history: make([]string, 0, 1000), - histIdx: -1, - inputBuffer: make([]rune, 0, 1024), - resizeCh: make(chan os.Signal, 1), - ctx: ctx, - cancel: cancel, - } -} - -func (t *Terminal) writeToStdout(data []byte) { - t.stdoutMutex.Lock() - defer t.stdoutMutex.Unlock() - os.Stdout.Write(data) - os.Stdout.Sync() -} - -// func (t *Terminal) detectEditorMode(cmd string) { -// editors := []string{"vim", "vi", "nano", "emacs"} -// for _, editor := range editors { -// if strings.Contains(cmd, editor) { -// t.isEditorMode = true -// return -// } -// } -// t.isEditorMode = false -// } - -// // Add new function to detect editor mode from output -// func (t *Terminal) checkOutputForEditorMode(output string) { -// // Common editor prompts/indicators -// editorIndicators := []string{ -// "~", // Vim's empty line indicator -// "-- INSERT --", // Vim's insert mode -// "-- NORMAL --", // Vim's normal mode -// "GNU nano", // Nano's header -// "File Edit", // Nano's menu -// "Emacs", // Emacs indicator -// } - -// // Check for editor exit indicators -// exitIndicators := []string{ -// "E325: ATTENTION", // Vim swap file message -// "File written", // Nano save message -// "File saved", // Nano save message -// "Wrote", // Vim write message -// "Quit", // Common exit message -// "exit", // Common exit message -// } - -// // First check for exit indicators -// for _, indicator := range exitIndicators { -// if strings.Contains(output, indicator) { -// t.isEditorMode = false -// return -// } -// } - -// // Then check for editor indicators -// for _, indicator := range editorIndicators { -// if strings.Contains(output, indicator) { -// t.isEditorMode = true -// return -// } -// } -// } - -func (t *Terminal) handleInput(data []byte) bool { - if len(data) == 0 { - return false - } - - // Handle critical control sequences locally - r := rune(data[0]) - switch r { - case 0x03: // Ctrl+C - if time.Since(t.lastCtrlCTime) < time.Second { - t.cancel() - t.wsClient.Close() - t.writeToStdout([]byte("\nExiting...\n")) - return true - } - t.lastCtrlCTime = time.Now() - t.inputBuffer = t.inputBuffer[:0] - t.cursorPos = 0 - t.redrawInputLine() - t.writeToStdout([]byte("^C\n")) - case 0x04: // Ctrl+D - if len(t.inputBuffer) == 0 { - t.cancel() - t.wsClient.Close() - t.writeToStdout([]byte("exit\n")) - return true - } - } - - // Send everything as stdin to remote terminal - msg := ws.NewMessage(ws.MessageTypeStdin, data, t.wsClient.GetMicroserviceUUID(), t.wsClient.GetExecID()) - t.wsClient.SendMessage(msg) - return false -} - -func (t *Terminal) redrawInputLine() { - t.stdoutMutex.Lock() - defer t.stdoutMutex.Unlock() - - // Clear the current line and move to start - os.Stdout.Write([]byte("\r\x1b[K")) - - // Redraw the prompt and input - if t.prompt != "" { - os.Stdout.Write([]byte(t.prompt)) - } - os.Stdout.Write([]byte(string(t.inputBuffer))) - - // Move cursor to end of input - t.cursorPos = len(t.inputBuffer) - os.Stdout.Write([]byte("\r")) // Move to start of line first - if t.prompt != "" { - os.Stdout.Write([]byte(t.prompt)) // Move past prompt - } - if t.cursorPos > 0 { - os.Stdout.Write([]byte(fmt.Sprintf("\x1b[%dC", t.cursorPos))) // Move to cursor position - } - os.Stdout.Sync() -} - -// func (t *Terminal) moveCursor(n int) { -// t.stdoutMutex.Lock() -// defer t.stdoutMutex.Unlock() -// if n > 0 { -// os.Stdout.Write([]byte(fmt.Sprintf("\x1b[%dC", n))) -// } else if n < 0 { -// os.Stdout.Write([]byte(fmt.Sprintf("\x1b[%dD", -n))) -// } -// os.Stdout.Sync() -// } - -// func (t *Terminal) moveCursorTo(pos int) { -// t.stdoutMutex.Lock() -// defer t.stdoutMutex.Unlock() - -// // Calculate the absolute position including prompt length (only add once) -// promptLen := len([]rune(t.prompt)) -// absPos := promptLen + pos - -// // Move cursor to the beginning of the line -// os.Stdout.Write([]byte("\r")) - -// // Move cursor to the correct position -// if absPos > 0 { -// os.Stdout.Write([]byte(fmt.Sprintf("\x1b[%dC", absPos))) -// } - -// t.cursorPos = pos -// os.Stdout.Sync() -// } - -// func (t *Terminal) replaceInputLine(newLine string) { -// t.stdoutMutex.Lock() -// defer t.stdoutMutex.Unlock() - -// // Clear the current line and move to start -// os.Stdout.Write([]byte("\r\x1b[K")) - -// // Update the buffer -// t.inputBuffer = []rune(newLine) - -// // Redraw the prompt and input -// if t.prompt != "" { -// os.Stdout.Write([]byte(t.prompt)) -// } -// os.Stdout.Write([]byte(string(t.inputBuffer))) - -// // Move cursor to end of input -// t.cursorPos = len(t.inputBuffer) -// os.Stdout.Write([]byte("\r")) // Move to start of line first -// if t.prompt != "" { -// os.Stdout.Write([]byte(t.prompt)) // Move past prompt -// } -// if t.cursorPos > 0 { -// os.Stdout.Write([]byte(fmt.Sprintf("\x1b[%dC", t.cursorPos))) // Move to cursor position -// } -// os.Stdout.Sync() -// } - -func (t *Terminal) cleanup() { - t.cleanupOnce.Do(func() { - if t.wsClient != nil { - t.wsClient.Close() - } - if t.oldState != nil { - term.Restore(int(os.Stdin.Fd()), t.oldState) - t.oldState = nil - } - }) -} - -func (t *Terminal) Start() error { - var err error - t.oldState, err = term.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - return fmt.Errorf("failed to set terminal to raw mode: %v", err) - } - defer t.cleanup() - - // Set up signal handling for window resize - signal.Notify(t.resizeCh, unix.SIGWINCH) - go t.handleResize() - - // Create error channel to coordinate exit - errCh := make(chan error, 1) - - // Monitor output and errors - go func() { - for { - select { - case <-t.ctx.Done(): - return - default: - msg, err := t.wsClient.ReadMessage() - if err != nil { - // Check if this is a normal closure - if t.wsClient.IsNormalClosure(err) { - // Normal closure - don't report as error, just exit gracefully - t.cancel() - return - } - // This is an actual error - pass it to exec layer for handling - errCh <- err - t.cancel() - return - } - if msg == nil { - // Normal termination (no error, no message) - t.cancel() - return - } - output := string(msg.Data) - t.writeToStdout([]byte(output)) - } - } - }() - - // Check for initial WebSocket errors - if err := t.wsClient.GetError(); err != nil { - // Check if this is a normal closure - if t.wsClient.IsNormalClosure(err) { - // Normal closure - don't report as error - t.cancel() - return nil - } - // This is an actual error - pass it to exec layer for handling - t.cancel() - return err - } - - // Start input handling in a separate goroutine - inputDone := make(chan struct{}) - go func() { - defer close(inputDone) - buf := make([]byte, 1) - for { - select { - case <-t.ctx.Done(): - return - default: - n, err := os.Stdin.Read(buf) - if err != nil || n == 0 { - continue - } - - // Handle input - if t.handleInput(buf[:n]) { - t.cancel() - return - } - } - } - }() - - // Wait for either error or context cancellation - select { - case <-t.ctx.Done(): - return nil - case err := <-errCh: - t.cancel() - return err - case <-inputDone: - return nil - } -} - -func (t *Terminal) handleResize() { - for range t.resizeCh { - // Local resize only; no forwarding - } -} - -func (t *Terminal) Stop() { - t.cancel() - t.cleanup() -} diff --git a/internal/util/terminal/terminal_windows.go b/internal/util/terminal/terminal_windows.go deleted file mode 100644 index db91beb17..000000000 --- a/internal/util/terminal/terminal_windows.go +++ /dev/null @@ -1,247 +0,0 @@ -//go:build windows -// +build windows - -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package terminal - -import ( - "context" - "fmt" - "os" - "sync" - "time" - - ws "github.com/eclipse-iofog/iofogctl/internal/util/websocket" - "golang.org/x/term" -) - -type Terminal struct { - wsClient *ws.Client - oldState *term.State - history []string - histIdx int - inputBuffer []rune - cursorPos int - resizeCh chan os.Signal - // lastCommand string - lastCtrlCTime time.Time - prompt string - stdoutMutex sync.Mutex - ctx context.Context - cancel context.CancelFunc - cleanupOnce sync.Once - // isEditorMode bool -} - -// var promptPattern = regexp.MustCompile(`(?m)^.*[@].*[$#] ?$`) - -func NewTerminal(wsClient *ws.Client) *Terminal { - ctx, cancel := context.WithCancel(context.Background()) - return &Terminal{ - wsClient: wsClient, - history: make([]string, 0, 1000), - histIdx: -1, - inputBuffer: make([]rune, 0, 1024), - resizeCh: make(chan os.Signal, 1), - ctx: ctx, - cancel: cancel, - } -} - -func (t *Terminal) writeToStdout(data []byte) { - t.stdoutMutex.Lock() - defer t.stdoutMutex.Unlock() - os.Stdout.Write(data) - os.Stdout.Sync() -} - -func (t *Terminal) handleInput(data []byte) bool { - if len(data) == 0 { - return false - } - - // Handle critical control sequences locally - r := rune(data[0]) - switch r { - case 0x03: // Ctrl+C - if time.Since(t.lastCtrlCTime) < time.Second { - t.cancel() - t.wsClient.Close() - t.writeToStdout([]byte("\nExiting...\n")) - return true - } - t.lastCtrlCTime = time.Now() - t.inputBuffer = t.inputBuffer[:0] - t.cursorPos = 0 - t.redrawInputLine() - t.writeToStdout([]byte("^C\n")) - case 0x04: // Ctrl+D - if len(t.inputBuffer) == 0 { - t.cancel() - t.wsClient.Close() - t.writeToStdout([]byte("exit\n")) - return true - } - } - - // Send everything as stdin to remote terminal - msg := ws.NewMessage(ws.MessageTypeStdin, data, t.wsClient.GetMicroserviceUUID(), t.wsClient.GetExecID()) - t.wsClient.SendMessage(msg) - return false -} - -func (t *Terminal) redrawInputLine() { - t.stdoutMutex.Lock() - defer t.stdoutMutex.Unlock() - - // Clear the current line and move to start - os.Stdout.Write([]byte("\r\x1b[K")) - - // Redraw the prompt and input - if t.prompt != "" { - os.Stdout.Write([]byte(t.prompt)) - } - os.Stdout.Write([]byte(string(t.inputBuffer))) - - // Move cursor to end of input - t.cursorPos = len(t.inputBuffer) - os.Stdout.Write([]byte("\r")) // Move to start of line first - if t.prompt != "" { - os.Stdout.Write([]byte(t.prompt)) // Move past prompt - } - if t.cursorPos > 0 { - os.Stdout.Write([]byte(fmt.Sprintf("\x1b[%dC", t.cursorPos))) // Move to cursor position - } - os.Stdout.Sync() -} - -func (t *Terminal) cleanup() { - t.cleanupOnce.Do(func() { - if t.wsClient != nil { - t.wsClient.Close() - } - if t.oldState != nil { - term.Restore(int(os.Stdin.Fd()), t.oldState) - t.oldState = nil - } - }) -} - -func (t *Terminal) Start() error { - var err error - t.oldState, err = term.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - return fmt.Errorf("failed to set terminal to raw mode: %v", err) - } - defer t.cleanup() - - // Windows doesn't support SIGWINCH, so we skip resize handling - // signal.Notify(t.resizeCh, syscall.SIGWINCH) - // go t.handleResize() - - // Create error channel to coordinate exit - errCh := make(chan error, 1) - - // Monitor output and errors - go func() { - for { - select { - case <-t.ctx.Done(): - return - default: - msg, err := t.wsClient.ReadMessage() - if err != nil { - // Check if this is a normal closure - if t.wsClient.IsNormalClosure(err) { - // Normal closure - don't report as error, just exit gracefully - t.cancel() - return - } - // This is an actual error - pass it to exec layer for handling - errCh <- err - t.cancel() - return - } - if msg == nil { - // Normal termination (no error, no message) - t.cancel() - return - } - output := string(msg.Data) - t.writeToStdout([]byte(output)) - } - } - }() - - // Check for initial WebSocket errors - if err := t.wsClient.GetError(); err != nil { - // Check if this is a normal closure - if t.wsClient.IsNormalClosure(err) { - // Normal closure - don't report as error - t.cancel() - return nil - } - // This is an actual error - pass it to exec layer for handling - t.cancel() - return err - } - - // Start input handling in a separate goroutine - inputDone := make(chan struct{}) - go func() { - defer close(inputDone) - buf := make([]byte, 1) - for { - select { - case <-t.ctx.Done(): - return - default: - n, err := os.Stdin.Read(buf) - if err != nil || n == 0 { - continue - } - - // Handle input - if t.handleInput(buf[:n]) { - t.cancel() - return - } - } - } - }() - - // Wait for either error or context cancellation - select { - case <-t.ctx.Done(): - return nil - case err := <-errCh: - t.cancel() - return err - case <-inputDone: - return nil - } -} - -// func (t *Terminal) handleResize() { -// // Windows doesn't support SIGWINCH, so this function is empty -// for range t.resizeCh { -// // No-op on Windows -// } -// } - -func (t *Terminal) Stop() { - t.cancel() - t.cleanup() -} diff --git a/internal/util/update_openid_client.go b/internal/util/update_openid_client.go index cf7cbc96e..ba3a29b46 100644 --- a/internal/util/update_openid_client.go +++ b/internal/util/update_openid_client.go @@ -1,171 +1,10 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "golang.org/x/oauth2/clientcredentials" - "github.com/eclipse-iofog/iofogctl/internal/resource" ) -// UpdateECNViewerClientRootURL updates the root URL for the ecnviewerclient -// using the controller client secret to obtain an admin token via OAuth2 -func UpdateECNViewerClientRootURL(auth resource.Auth, newRootURL string) error { - // Validate input parameters - if auth.URL == "" { - return fmt.Errorf("auth URL is required") - } - if auth.Realm == "" { - return fmt.Errorf("auth realm is required") - } - if auth.ControllerClient == "" { - return fmt.Errorf("controller client ID is required") - } - if auth.ControllerSecret == "" { - return fmt.Errorf("controller client secret is required") - } - if auth.ViewerClient == "" { - return fmt.Errorf("viewer client ID is required") - } - if newRootURL == "" { - return fmt.Errorf("new root URL is required") - } - - // Configure OAuth2 client credentials - tokenURL := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/token", auth.URL, auth.Realm) - config := &clientcredentials.Config{ - ClientID: auth.ControllerClient, - ClientSecret: auth.ControllerSecret, - TokenURL: tokenURL, - Scopes: []string{"openid", "profile", "email"}, - } - - // Obtain access token - ctx := context.Background() - token, err := config.Token(ctx) - if err != nil { - return fmt.Errorf("failed to obtain access token: %v", err) - } - - // Get the client ID (internal Keycloak ID) for the viewer client - clientID, err := getKeycloakClientID(auth, auth.ViewerClient, token.AccessToken) - if err != nil { - return fmt.Errorf("failed to get client ID: %v", err) - } - - // Update the root URL directly - err = updateClientRootURL(auth, clientID, newRootURL, token.AccessToken) - if err != nil { - return fmt.Errorf("failed to update client root URL: %v", err) - } - - return nil -} - -// getKeycloakClientID retrieves the internal Keycloak ID for a client by its clientId -func getKeycloakClientID(auth resource.Auth, clientID, adminToken string) (string, error) { - // Construct admin API URL - adminURL := fmt.Sprintf("%s/admin/realms/%s/clients", auth.URL, auth.Realm) - - // Create request - req, err := http.NewRequest("GET", adminURL, nil) - if err != nil { - return "", fmt.Errorf("failed to create get client request: %v", err) - } - - req.Header.Set("Authorization", "Bearer "+adminToken) - req.Header.Set("Accept", "application/json") - - // Execute request - client := &http.Client{Timeout: 30 * time.Second} - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("failed to execute get client request: %v", err) - } - defer resp.Body.Close() - - // Check response status - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("get client request failed with status %d: %s", resp.StatusCode, string(body)) - } - - // Parse response to find the specific client - var clients []map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&clients); err != nil { - return "", fmt.Errorf("failed to decode clients response: %v", err) - } - - // Find the client with matching clientID - for _, client := range clients { - if clientIDValue, ok := client["clientId"].(string); ok && clientIDValue == clientID { - if id, ok := client["id"].(string); ok { - return id, nil - } - } - } - - return "", fmt.Errorf("client with ID '%s' not found", clientID) -} - -// updateClientRootURL updates the root URL for a specific client using its internal ID -func updateClientRootURL(auth resource.Auth, clientID, newRootURL, adminToken string) error { - // Construct admin API URL - adminURL := fmt.Sprintf("%s/admin/realms/%s/clients/%s", auth.URL, auth.Realm, clientID) - - // Create the update payload with only the root URL - updatePayload := map[string]interface{}{ - "rootUrl": newRootURL, - } - - // Marshal to JSON - payloadJSON, err := json.Marshal(updatePayload) - if err != nil { - return fmt.Errorf("failed to marshal update payload: %v", err) - } - - // Create request - req, err := http.NewRequest("PUT", adminURL, bytes.NewBuffer(payloadJSON)) - if err != nil { - return fmt.Errorf("failed to create update client request: %v", err) - } - - req.Header.Set("Authorization", "Bearer "+adminToken) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - // Execute request - httpClient := &http.Client{Timeout: 30 * time.Second} - resp, err := httpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to execute update client request: %v", err) - } - defer resp.Body.Close() - - // Check response status - if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("update client request failed with status %d: %s", resp.StatusCode, string(body)) - } - +// UpdateECNViewerClientRootURL is retired in v3.8 (Keycloak viewer client replaced by embedded/external OIDC). +func UpdateECNViewerClientRootURL(_ resource.Auth, _ string) error { return nil } diff --git a/internal/util/websocket/client.go b/internal/util/websocket/client.go deleted file mode 100644 index af957eb6f..000000000 --- a/internal/util/websocket/client.go +++ /dev/null @@ -1,349 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package websocket - -import ( - "crypto/tls" - "fmt" - "net/http" - "strings" - "sync" - "time" - - "github.com/eclipse-iofog/iofogctl/pkg/util" - "github.com/gorilla/websocket" -) - -// Client represents a WebSocket client -type Client struct { - conn *websocket.Conn - microserviceUUID string - execID string - done chan struct{} - lastMessageType int // Track the last message type - err error - errMutex sync.Mutex - closeOnce sync.Once - // Keep-alive fields - pingTicker *time.Ticker - lastPongTime time.Time - pongMutex sync.RWMutex -} - -// NewClient creates a new WebSocket client -func NewClient(microserviceUUID string) *Client { - return &Client{ - microserviceUUID: microserviceUUID, - done: make(chan struct{}), - } -} - -// Connect establishes a WebSocket connection to the server -func (c *Client) Connect(url string, headers http.Header) error { - dialer := websocket.Dialer{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - HandshakeTimeout: 45 * time.Second, - ReadBufferSize: 1024, - WriteBufferSize: 1024, - } - - conn, resp, err := dialer.Dial(url, headers) - if err != nil { - // Check if this is a close frame error - if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { - if closeErr, ok := err.(*websocket.CloseError); ok { - c.errMutex.Lock() - c.err = util.NewError(fmt.Sprintf("connection closed by server (code: %d, reason: %s)", closeErr.Code, closeErr.Text)) - c.errMutex.Unlock() - c.Close() - return c.err - } - } - // Check if we got a non-200 response - if resp != nil && resp.StatusCode != http.StatusSwitchingProtocols { - c.errMutex.Lock() - c.err = util.NewError(fmt.Sprintf("failed to establish WebSocket connection: server returned status %d", resp.StatusCode)) - c.errMutex.Unlock() - c.Close() - return c.err - } - c.errMutex.Lock() - c.err = util.NewError(fmt.Sprintf("failed to establish WebSocket connection: %v", err)) - c.errMutex.Unlock() - c.Close() - return c.err - } - - c.conn = conn - - // Set up ping/pong handlers - c.setupPingPong() - - // Start ping loop - c.startPingLoop() - - return nil -} - -// setupPingPong sets up ping/pong handlers for keep-alive -func (c *Client) setupPingPong() { - // Handle incoming pings from server - c.conn.SetPingHandler(func(appData string) error { - // Respond with pong immediately - return c.conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(DefaultPongTimeout*time.Millisecond)) - }) - - // Handle incoming pongs from server - c.conn.SetPongHandler(func(appData string) error { - c.pongMutex.Lock() - c.lastPongTime = time.Now() - c.pongMutex.Unlock() - - // Extend read deadline - return c.conn.SetReadDeadline(time.Now().Add(DefaultPingInterval * 2 * time.Millisecond)) - }) - - // Set initial read deadline - c.conn.SetReadDeadline(time.Now().Add(DefaultPingInterval * 2 * time.Millisecond)) -} - -// startPingLoop starts the periodic ping sending loop -func (c *Client) startPingLoop() { - c.pingTicker = time.NewTicker(DefaultPingInterval * time.Millisecond) - - go func() { - defer c.pingTicker.Stop() - - for { - select { - case <-c.pingTicker.C: - if !c.IsConnected() { - return - } - - // Send ping to server - if err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(DefaultPongTimeout*time.Millisecond)); err != nil { - c.handlePingError(err) - return - } - - // Check pong timeout in background - go c.checkPongTimeout() - - case <-c.done: - return - } - } - }() -} - -// checkPongTimeout checks if we received a pong response within timeout -func (c *Client) checkPongTimeout() { - time.Sleep(DefaultPongTimeout * time.Millisecond) - - // Check if connection is still active - if !c.IsConnected() { - return // Connection already closed, no need to check timeout - } - - c.pongMutex.RLock() - lastPong := c.lastPongTime - c.pongMutex.RUnlock() - - if time.Since(lastPong) > DefaultPongTimeout*time.Millisecond { - c.errMutex.Lock() - c.err = util.NewError("pong timeout - server not responding") - c.errMutex.Unlock() - c.Close() - } -} - -// handlePingError handles ping sending errors -func (c *Client) handlePingError(err error) { - // Check if this is a normal closure error - if c.IsNormalClosure(err) { - // Normal closure - don't treat as error - c.Close() - return - } - - // This is an actual ping error - c.errMutex.Lock() - c.err = util.NewError(fmt.Sprintf("ping failed: %v", err)) - c.errMutex.Unlock() - c.Close() -} - -// SendMessage sends a message to the server -func (c *Client) SendMessage(msg *Message) error { - if c.conn == nil { - return util.NewError("not connected") - } - - // Set session-specific fields - msg.MicroserviceUUID = c.microserviceUUID - msg.ExecID = c.execID - - data, err := msg.Encode() - if err != nil { - return util.NewError(fmt.Sprintf("failed to encode message: %v", err)) - } - - return c.conn.WriteMessage(websocket.BinaryMessage, data) -} - -// ReadMessage reads a message from the server -func (c *Client) ReadMessage() (*Message, error) { - if c.conn == nil { - return nil, util.NewError("not connected") - } - - messageType, data, err := c.conn.ReadMessage() - if err != nil { - // Check if this is a normal closure - if c.IsNormalClosure(err) { - // Normal closure - don't treat as error, just close gracefully - c.Close() - return nil, nil // Return nil to indicate normal termination - } - - // This is an actual error - format and store it - if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { - // Extract close code and reason if available - if closeErr, ok := err.(*websocket.CloseError); ok { - err = util.NewError(fmt.Sprintf("connection closed by server (code: %d, reason: %s)", closeErr.Code, closeErr.Text)) - } - } else { - err = util.NewError(fmt.Sprintf("failed to read message: %v", err)) - } - // Store the error and close connection - c.errMutex.Lock() - c.err = err - c.errMutex.Unlock() - c.Close() - return nil, err - } - - // Store the message type - c.lastMessageType = messageType - - // All messages are now MessagePack encoded - msg, err := Decode(data) - if err != nil { - // Store the error and close connection - c.errMutex.Lock() - c.err = err - c.errMutex.Unlock() - c.Close() - return nil, err - } - - // Update session-specific fields - c.execID = msg.ExecID - - // Update last pong time (activity detected) - c.pongMutex.Lock() - c.lastPongTime = time.Now() - c.pongMutex.Unlock() - - // Extend read deadline - c.conn.SetReadDeadline(time.Now().Add(DefaultPingInterval * 2 * time.Millisecond)) - - return msg, nil -} - -// Close closes the WebSocket connection -func (c *Client) Close() error { - var closeErr error - c.closeOnce.Do(func() { - // Stop ping ticker - if c.pingTicker != nil { - c.pingTicker.Stop() - } - - if c.conn != nil { - // Send close message before closing - closeMsg := NewMessage(MessageTypeClose, nil, c.microserviceUUID, c.execID) - _ = c.SendMessage(closeMsg) - closeErr = c.conn.Close() - c.conn = nil - } - close(c.done) - }) - return closeErr -} - -// IsConnected checks if the client is connected -func (c *Client) IsConnected() bool { - return c.conn != nil -} - -// GetDone returns the done channel -func (c *Client) GetDone() <-chan struct{} { - return c.done -} - -// SetExecID sets the execution ID for the client -func (c *Client) SetExecID(execID string) { - c.execID = execID -} - -// GetExecID returns the current execution ID -func (c *Client) GetExecID() string { - return c.execID -} - -// GetMicroserviceUUID returns the microservice UUID -func (c *Client) GetMicroserviceUUID() string { - return c.microserviceUUID -} - -// GetError returns the last error that occurred -func (c *Client) GetError() error { - c.errMutex.Lock() - defer c.errMutex.Unlock() - return c.err -} - -// IsNormalClosure checks if the error represents a normal session closure -func (c *Client) IsNormalClosure(err error) bool { - if err == nil { - return false - } - - // Check for WebSocket close errors with normal codes - if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { - return true - } - - // Check for "use of closed network connection" during intentional close - if closeErr, ok := err.(*websocket.CloseError); ok { - return closeErr.Code == websocket.CloseNormalClosure || - closeErr.Code == websocket.CloseGoingAway || - closeErr.Code == websocket.CloseNoStatusReceived - } - - // Check for network errors that occur during intentional close - errStr := err.Error() - if strings.Contains(errStr, "use of closed network connection") || - strings.Contains(errStr, "connection reset by peer") || - strings.Contains(errStr, "broken pipe") { - // These are expected when we intentionally close the connection - return true - } - - return false -} diff --git a/internal/util/websocket/constants.go b/internal/util/websocket/constants.go deleted file mode 100644 index 326a38dff..000000000 --- a/internal/util/websocket/constants.go +++ /dev/null @@ -1,38 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package websocket - -// Message types for WebSocket communication -const ( - MessageTypeStdin uint8 = 0 - MessageTypeStdout uint8 = 1 - MessageTypeStderr uint8 = 2 - MessageTypeControl uint8 = 3 - MessageTypeClose uint8 = 4 - MessageTypeActivation uint8 = 5 - MessageTypeLogLine uint8 = 6 - MessageTypeLogStart uint8 = 7 - MessageTypeLogStop uint8 = 8 - MessageTypeLogError uint8 = 9 -) - -// WebSocket configuration constants -const ( - DefaultPingInterval = 30000 // 30 seconds - DefaultPongTimeout = 10000 // 10 seconds - DefaultMaxPayload = 1024 * 1024 // 1MB - DefaultSessionTimeout = 300000 // 5 minutes - DefaultCleanupInterval = 60000 // 1 minute - DefaultMaxConnections = 10 -) diff --git a/internal/util/websocket/message.go b/internal/util/websocket/message.go deleted file mode 100644 index 9fd33fde9..000000000 --- a/internal/util/websocket/message.go +++ /dev/null @@ -1,110 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package websocket - -import ( - "fmt" - "time" - - "github.com/vmihailenco/msgpack/v5" -) - -// Message represents a WebSocket message with type and payload -type Message struct { - Type uint8 `msgpack:"type"` - Data []byte `msgpack:"data"` - MicroserviceUUID string `msgpack:"microserviceUuid"` - ExecID string `msgpack:"execId"` - Timestamp int64 `msgpack:"timestamp"` -} - -// NewMessage creates a new Message with the given type and payload -func NewMessage(msgType uint8, payload []byte, microserviceUUID string, execID string) *Message { - return &Message{ - Type: msgType, - Data: payload, - MicroserviceUUID: microserviceUUID, - ExecID: execID, - Timestamp: time.Now().UnixMilli(), - } -} - -// Encode encodes the message into MessagePack format -func (m *Message) Encode() ([]byte, error) { - if len(m.Data) > DefaultMaxPayload { - return nil, fmt.Errorf("payload size exceeds maximum allowed size of %d bytes", DefaultMaxPayload) - } - - // Encode using MessagePack - return msgpack.Marshal(m) -} - -// Decode decodes a MessagePack message into a Message struct -func Decode(data []byte) (*Message, error) { - var msg Message - if err := msgpack.Unmarshal(data, &msg); err != nil { - return nil, fmt.Errorf("failed to decode MessagePack data: %v", err) - } - return &msg, nil -} - -// IsControlMessage checks if the message is a control message -func (m *Message) IsControlMessage() bool { - return m.Type == MessageTypeControl -} - -// IsCloseMessage checks if the message is a close message -func (m *Message) IsCloseMessage() bool { - return m.Type == MessageTypeClose -} - -// IsActivationMessage checks if the message is an activation message -func (m *Message) IsActivationMessage() bool { - return m.Type == MessageTypeActivation -} - -// IsStdinMessage checks if the message is a stdin message -func (m *Message) IsStdinMessage() bool { - return m.Type == MessageTypeStdin -} - -// IsStdoutMessage checks if the message is a stdout message -func (m *Message) IsStdoutMessage() bool { - return m.Type == MessageTypeStdout -} - -// IsStderrMessage checks if the message is a stderr message -func (m *Message) IsStderrMessage() bool { - return m.Type == MessageTypeStderr -} - -// IsLogLineMessage checks if the message is a log line message -func (m *Message) IsLogLineMessage() bool { - return m.Type == MessageTypeLogLine -} - -// IsLogStartMessage checks if the message is a log start message -func (m *Message) IsLogStartMessage() bool { - return m.Type == MessageTypeLogStart -} - -// IsLogStopMessage checks if the message is a log stop message -func (m *Message) IsLogStopMessage() bool { - return m.Type == MessageTypeLogStop -} - -// IsLogErrorMessage checks if the message is a log error message -func (m *Message) IsLogErrorMessage() bool { - return m.Type == MessageTypeLogError -} diff --git a/internal/util/websocket/session.go b/internal/util/websocket/session.go deleted file mode 100644 index ae5c8414f..000000000 --- a/internal/util/websocket/session.go +++ /dev/null @@ -1,103 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package websocket - -import ( - "sync" - "time" -) - -// Session represents a WebSocket session between a user and an agent -type Session struct { - ExecID string - MicroserviceUUID string - LastActivity time.Time - mu sync.RWMutex -} - -// NewSession creates a new Session with the given execID and microserviceUUID -func NewSession(execID, microserviceUUID string) *Session { - return &Session{ - ExecID: execID, - MicroserviceUUID: microserviceUUID, - LastActivity: time.Now(), - } -} - -// UpdateActivity updates the last activity timestamp -func (s *Session) UpdateActivity() { - s.mu.Lock() - defer s.mu.Unlock() - s.LastActivity = time.Now() -} - -// IsExpired checks if the session has expired based on the timeout -func (s *Session) IsExpired(timeout time.Duration) bool { - s.mu.RLock() - defer s.mu.RUnlock() - return time.Since(s.LastActivity) > timeout -} - -// SessionManager manages WebSocket sessions -type SessionManager struct { - sessions map[string]*Session - mu sync.RWMutex -} - -// NewSessionManager creates a new SessionManager -func NewSessionManager() *SessionManager { - return &SessionManager{ - sessions: make(map[string]*Session), - } -} - -// AddSession adds a new session to the manager -func (sm *SessionManager) AddSession(session *Session) { - sm.mu.Lock() - defer sm.mu.Unlock() - sm.sessions[session.ExecID] = session -} - -// GetSession retrieves a session by its execID -func (sm *SessionManager) GetSession(execID string) *Session { - sm.mu.RLock() - defer sm.mu.RUnlock() - return sm.sessions[execID] -} - -// RemoveSession removes a session by its execID -func (sm *SessionManager) RemoveSession(execID string) { - sm.mu.Lock() - defer sm.mu.Unlock() - delete(sm.sessions, execID) -} - -// CleanupExpiredSessions removes all expired sessions -func (sm *SessionManager) CleanupExpiredSessions(timeout time.Duration) { - sm.mu.Lock() - defer sm.mu.Unlock() - - for execID, session := range sm.sessions { - if session.IsExpired(timeout) { - delete(sm.sessions, execID) - } - } -} - -// GetActiveSessions returns the number of active sessions -func (sm *SessionManager) GetActiveSessions() int { - sm.mu.RLock() - defer sm.mu.RUnlock() - return len(sm.sessions) -} diff --git a/internal/validate/password.go b/internal/validate/password.go new file mode 100644 index 000000000..3a569459a --- /dev/null +++ b/internal/validate/password.go @@ -0,0 +1,39 @@ +package validate + +import ( + "strings" + "unicode" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +// ValidatePasswordComplexity returns an InputError when password fails v3.8 policy: +// at least 12 characters, one uppercase letter, and one special (non-alphanumeric) character. +// +//nolint:revive // ValidatePasswordComplexity matches the validate package naming convention. +func ValidatePasswordComplexity(password string) error { + var failures []string + if len(password) < 12 { + failures = append(failures, "at least 12 characters") + } + hasUpper := false + hasSpecial := false + for _, r := range password { + if unicode.IsUpper(r) { + hasUpper = true + } + if !unicode.IsLetter(r) && !unicode.IsDigit(r) { + hasSpecial = true + } + } + if !hasUpper { + failures = append(failures, "at least one uppercase letter") + } + if !hasSpecial { + failures = append(failures, "at least one special character") + } + if len(failures) == 0 { + return nil + } + return util.NewInputError("password must contain " + strings.Join(failures, ", ")) +} diff --git a/internal/validate/password_test.go b/internal/validate/password_test.go new file mode 100644 index 000000000..879ea7ac1 --- /dev/null +++ b/internal/validate/password_test.go @@ -0,0 +1,25 @@ +package validate + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidatePasswordComplexity(t *testing.T) { + t.Run("valid", func(t *testing.T) { + require.NoError(t, ValidatePasswordComplexity("LocalTest12!")) + }) + + t.Run("too short", func(t *testing.T) { + require.Error(t, ValidatePasswordComplexity("Short1!")) + }) + + t.Run("no uppercase", func(t *testing.T) { + require.Error(t, ValidatePasswordComplexity("localtest12!")) + }) + + t.Run("no special", func(t *testing.T) { + require.Error(t, ValidatePasswordComplexity("LocalTest1234")) + }) +} diff --git a/iofogctl-logo.png b/iofogctl-logo.png deleted file mode 100644 index 9485e1eeb..000000000 Binary files a/iofogctl-logo.png and /dev/null differ diff --git a/pipeline/ha.yaml b/pipeline/ha.yaml deleted file mode 100644 index 5a43e8018..000000000 --- a/pipeline/ha.yaml +++ /dev/null @@ -1,67 +0,0 @@ -jobs: -- job: HA - pool: - vmImage: 'Ubuntu-20.04' - steps: - - task: DownloadBuildArtifacts@0 - displayName: 'Download Build Artifacts' - inputs: - artifactName: iofogctl - downloadPath: $(System.DefaultWorkingDirectory) - - script: | - sudo cp iofogctl/build_linux_linux_amd64/iofogctl /usr/local/bin/ - sudo chmod 0755 /usr/local/bin/iofogctl - - template: steps/postinstall.yaml - - template: steps/init-ssh.yaml - - template: steps/init-vms.yaml - parameters: - id: $(jobuuid) - distro: $(gcp.vm.distro.xenial) - repo: $(gcp.vm.repo.ubuntu) - agent_count: 2 - controller_count: 0 - - script: | - set -e - keyFilePath="$(Agent.TempDirectory)/azure-gcp.json" - - # Install gcloud-auth-plugin - echo "deb https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list - curl -f https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - - sudo apt-get update && sudo apt-get install -y google-cloud-sdk-gke-gcloud-auth-plugin - - gcloud components list - - gcloud --quiet auth activate-service-account --key-file="${keyFilePath}" - gcloud --quiet container clusters get-credentials $(gcp.cluster.name) --region $(gcp.cluster.region) - displayName: 'Connect to cluster' - - template: steps/configure-remote-tests.yaml - - script: | - sed -i "s|DB_PROVIDER=.*|DB_PROVIDER=\"postgres\"|g" test/env.sh - sed -i "s|DB_USER=.*|DB_USER=\"$(db.user)\"|g" test/env.sh - sed -i "s|DB_HOST=.*|DB_HOST=\"postgres-postgresql.postgres.svc.cluster.local\"|g" test/env.sh - sed -i "s|DB_PORT=.*|DB_PORT=5432|g" test/env.sh - sed -i "s|DB_PW=.*|DB_PW=\"$(db.pw)\"|g" test/env.sh - sed -i "s|DB_NAME=.*|DB_NAME=\"iofog$(jobuuid)\"|g" test/env.sh - sed -i "s|CONTROLLER_IMAGE=.*|CONTROLLER_IMAGE=\"$(enterprise_image)\"|g" test/env.sh - cp test/env.sh test/conf - cat test/conf/env.sh - displayName: 'Set up Postgres on K8s cluster' - - template: steps/install-test-deps.yaml - - script: | - set -o pipefail - test/run.bash ha | tee test/conf/results-ha.tap - displayName: 'Run Functional Tests' - - script: | - tap-junit -i test/conf/results-ha.tap -o test/conf -s HA -n results-ha.xml || true - displayName: 'Convert test output from TAP to JUnit' - condition: succeededOrFailed() - - script: | - test/clean.bash - displayName: 'Clean K8s Cluster' - condition: always() - - template: steps/functional-post-test.yaml - - template: steps/functional-clean-vm.yaml - parameters: - id: $(jobuuid) - agent_count: 2 - controller_count: 0 diff --git a/pipeline/k8s.yaml b/pipeline/k8s.yaml deleted file mode 100644 index 7498c9705..000000000 --- a/pipeline/k8s.yaml +++ /dev/null @@ -1,56 +0,0 @@ -jobs: -- job: K8s - pool: - vmImage: 'Ubuntu-20.04' - steps: - - task: DownloadBuildArtifacts@0 - displayName: 'Download Build Artifacts' - inputs: - artifactName: iofogctl - downloadPath: $(System.DefaultWorkingDirectory) - - script: | - sudo cp iofogctl/build_linux_linux_amd64/iofogctl /usr/local/bin/ - sudo chmod 0755 /usr/local/bin/iofogctl - - template: steps/postinstall.yaml - - template: steps/init-ssh.yaml - - template: steps/init-vms.yaml - parameters: - id: $(jobuuid) - distro: $(gcp.vm.distro.buster) - repo: $(gcp.vm.repo.debian) - agent_count: 2 - controller_count: 0 - - script: | - set -e - keyFilePath="$(Agent.TempDirectory)/azure-gcp.json" - - # Install gcloud-auth-plugin - echo "deb https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list - curl -f https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - - sudo apt-get update && sudo apt-get install -y google-cloud-sdk-gke-gcloud-auth-plugin - - gcloud components list - - gcloud --quiet auth activate-service-account --key-file="${keyFilePath}" - gcloud --quiet container clusters get-credentials $(gcp.cluster.name) --region $(gcp.cluster.region) - displayName: 'Connect to cluster' - - template: steps/configure-remote-tests.yaml - - template: steps/install-test-deps.yaml - - script: | - set -o pipefail - test/run.bash k8s | tee test/conf/results-k8s.tap - displayName: 'Run Functional Tests' - - script: | - tap-junit -i test/conf/results-k8s.tap -o test/conf -s K8s -n results-k8s.xml || true - displayName: 'Convert test output from TAP to JUnit' - condition: succeededOrFailed() - - script: | - test/clean.bash - displayName: 'Clean K8s Cluster' - condition: always() - - template: steps/functional-post-test.yaml - - template: steps/functional-clean-vm.yaml - parameters: - id: $(jobuuid) - agent_count: 2 - controller_count: 0 \ No newline at end of file diff --git a/pipeline/local.yaml b/pipeline/local.yaml deleted file mode 100644 index a2111563e..000000000 --- a/pipeline/local.yaml +++ /dev/null @@ -1,41 +0,0 @@ -jobs: -- job: Local - pool: - vmImage: 'Ubuntu-20.04' - steps: - - template: steps/init-gcloud-steps.yaml - - script: | - gcloud --quiet auth configure-docker - docker pull $(controller_image) - docker pull $(agent_image) - displayName: 'Pull develop gcr docker image' - - task: DownloadBuildArtifacts@0 - displayName: 'Download Build Artifacts' - inputs: - artifactName: iofogctl - downloadPath: $(System.DefaultWorkingDirectory) - - script: | - sudo cp iofogctl/build_linux_linux_amd64/iofogctl /usr/local/bin/ - sudo chmod 0755 /usr/local/bin/iofogctl - - bash: | - sudo apt-get install -y jq - displayName: 'Install jq' - - template: steps/postinstall.yaml - - template: steps/configure-remote-tests.yaml - - template: steps/install-test-deps.yaml - - script: | - test/run.bash smoke - displayName: 'Run Smoke Tests' - - script: | - set -o pipefail - test/run.bash local | tee test/conf/results-local.tap - displayName: 'Run Functional Tests' - - script: | - tap-junit -i test/conf/results-local.tap -o test/conf -s Local -n results-local.xml || true - displayName: 'Convert test output from TAP to JUnit' - condition: succeededOrFailed() - - template: steps/functional-post-test.yaml - - script: | - docker system prune -af - condition: always() - displayName: 'Clean local docker' \ No newline at end of file diff --git a/pipeline/steps/configure-env.sh b/pipeline/steps/configure-env.sh deleted file mode 100755 index 097bb9b9f..000000000 --- a/pipeline/steps/configure-env.sh +++ /dev/null @@ -1,17 +0,0 @@ - sed -i "s|AGENT_LIST=.*|AGENT_LIST=\"${{ env.agent_vm_list }}\"|g" test/env.sh - sed -i "s|VANILLA_CONTROLLER=.*|VANILLA_CONTROLLER=\"${{ env.controller_vm }}\"|g" test/env.sh - sed -i "s|NAMESPACE=.*|NAMESPACE=\"${{ github.run_number }}\"|g" test/env.sh - sed -i "s|CONTROLLER_IMAGE=.*|CONTROLLER_IMAGE=\"${{ env.controller_image }}\"|g" test/env.sh - sed -i "s|CONTROLLER_VANILLA_VERSION=.*|CONTROLLER_VANILLA_VERSION=\"${{ env.controller_version }}\"|g" test/env.sh - sed -i "s|OPERATOR_IMAGE=.*|OPERATOR_IMAGE=\"${{ env.operator_image }}\"|g" test/env.sh - sed -i "s|PORT_MANAGER_IMAGE=.*|PORT_MANAGER_IMAGE=\"${{ env.port_manager_image }}\"|g" test/env.sh - sed -i "s|AGENT_IMAGE=.*|AGENT_IMAGE=\"${{ env.agent_image }}\"|g" test/env.sh - sed -i "s|ROUTER_IMAGE=.*|ROUTER_IMAGE=\"${{ env.router_image }}\"|g" test/env.sh - sed -i "s|ROUTER_ARM_IMAGE=.*|ROUTER_ARM_IMAGE=\"${{ env.router_arm_image }}\"|g" test/env.sh - sed -i "s|PROXY_IMAGE=.*|PROXY_IMAGE=\"${{ env.proxy_image }}\"|g" test/env.sh - sed -i "s|PROXY_ARM_IMAGE=.*|PROXY_ARM_IMAGE=\"${{ env.proxy_arm_image }}\"|g" test/env.sh - sed -i "s|AGENT_VANILLA_VERSION=.*|AGENT_VANILLA_VERSION=\"${{ env.iofog_agent_version }}\"|g" test/env.sh - sed -i "s|CONTROLLER_PACKAGE_CLOUD_TOKEN=.*|CONTROLLER_PACKAGE_CLOUD_TOKEN=\"${{ env.pkg.controller.token }}\"|g" test/env.sh - sed -i "s|AGENT_PACKAGE_CLOUD_TOKEN=.*|AGENT_PACKAGE_CLOUD_TOKEN=\"${{ env.pkg.agent.token }}\"|g" test/env.sh - cp test/env.sh test/conf - cat test/conf/env.sh \ No newline at end of file diff --git a/pipeline/steps/configure-remote-tests.yaml b/pipeline/steps/configure-remote-tests.yaml deleted file mode 100644 index 307938ef5..000000000 --- a/pipeline/steps/configure-remote-tests.yaml +++ /dev/null @@ -1,35 +0,0 @@ -parameters: - windows: 'false' - -steps: -- bash: | - sed -i "s|AGENT_LIST=.*|AGENT_LIST=\"$(agent_vm_list)\"|g" test/env.sh - sed -i "s|VANILLA_CONTROLLER=.*|VANILLA_CONTROLLER=\"$(controller_vm)\"|g" test/env.sh - if [[ ${{ parameters.windows }} == "false" ]]; then - KCONF=$(echo "$HOME/.kube/config") - sed -i "s|TEST_KUBE_CONFIG=.*|TEST_KUBE_CONFIG=\"$KCONF\"|g" test/env.sh - fi - sed -i "s|KEY_FILE=.*|KEY_FILE=\"~/id_rsa\"|g" test/env.sh - keyFilePath="$(Agent.TempDirectory)/id_rsa" - if [[ ${{ parameters.windows }} == "true" ]]; then - keyFilePath=$(wslpath "${keyFilePath}") - fi - cat $keyFilePath > ~/id_rsa - echo $(ssh.user.pub) > ~/id_rsa.pub - NS=$(jobuuid) - sed -i "s|NAMESPACE=.*|NAMESPACE=\"$NS\"|g" test/env.sh - sed -i "s|CONTROLLER_IMAGE=.*|CONTROLLER_IMAGE=\"$(controller_image)\"|g" test/env.sh - sed -i "s|CONTROLLER_VANILLA_VERSION=.*|CONTROLLER_VANILLA_VERSION=\"$(controller_version)\"|g" test/env.sh - sed -i "s|OPERATOR_IMAGE=.*|OPERATOR_IMAGE=\"$(operator_image)\"|g" test/env.sh - sed -i "s|PORT_MANAGER_IMAGE=.*|PORT_MANAGER_IMAGE=\"$(port_manager_image)\"|g" test/env.sh - sed -i "s|AGENT_IMAGE=.*|AGENT_IMAGE=\"$(agent_image)\"|g" test/env.sh - sed -i "s|ROUTER_IMAGE=.*|ROUTER_IMAGE=\"$(router_image)\"|g" test/env.sh - sed -i "s|ROUTER_ARM_IMAGE=.*|ROUTER_ARM_IMAGE=\"$(router_arm_image)\"|g" test/env.sh - sed -i "s|PROXY_IMAGE=.*|PROXY_IMAGE=\"$(proxy_image)\"|g" test/env.sh - sed -i "s|PROXY_ARM_IMAGE=.*|PROXY_ARM_IMAGE=\"$(proxy_arm_image)\"|g" test/env.sh - sed -i "s|AGENT_VANILLA_VERSION=.*|AGENT_VANILLA_VERSION=\"$(iofog_agent_version)\"|g" test/env.sh - sed -i "s|CONTROLLER_PACKAGE_CLOUD_TOKEN=.*|CONTROLLER_PACKAGE_CLOUD_TOKEN=\"$(pkg.controller.token)\"|g" test/env.sh - sed -i "s|AGENT_PACKAGE_CLOUD_TOKEN=.*|AGENT_PACKAGE_CLOUD_TOKEN=\"$(pkg.agent.token)\"|g" test/env.sh - cp test/env.sh test/conf - cat test/conf/env.sh - displayName: 'Configure Remote Tests' \ No newline at end of file diff --git a/pipeline/steps/functional-clean-vm.yaml b/pipeline/steps/functional-clean-vm.yaml deleted file mode 100644 index aa1b7e82c..000000000 --- a/pipeline/steps/functional-clean-vm.yaml +++ /dev/null @@ -1,25 +0,0 @@ -parameters: - id: '' - agent_count: 0 - controller_count: 0 - -steps: -- bash: | - id=${{ parameters.id }} - agent_count=${{ parameters.agent_count }} - controller_count=${{ parameters.controller_count }} - jobs=0 - for idx in $(seq 1 $agent_count); do - gcloud compute --project=$(gcp.project.name) instances delete iofogctl-ci-$id-$idx --zone=$(gcp.vm.zone) --delete-disks=all -q & - ((jobs++)) - done - if [ $controller_count -gt 0 ]; then - idx=$((agent_count+1)) - gcloud compute --project=$(gcp.project.name) instances delete iofogctl-ci-$id-$idx --zone=$(gcp.vm.zone) --delete-disks=all -q & - ((jobs++)) - fi - for job in $(seq 1 $jobs); do - wait %$job - done - displayName: 'Teardown VMs' - condition: always() \ No newline at end of file diff --git a/pipeline/steps/functional-post-test.yaml b/pipeline/steps/functional-post-test.yaml deleted file mode 100644 index 712c6273d..000000000 --- a/pipeline/steps/functional-post-test.yaml +++ /dev/null @@ -1,16 +0,0 @@ -steps: -# Publish Test Results -# Publish test results to Azure Pipelines -- task: PublishTestResults@2 - condition: succeededOrFailed() - inputs: - testResultsFormat: 'JUnit' # Options: JUnit, NUnit, VSTest, xUnit, cTest - testResultsFiles: 'results-*.xml' - searchFolder: './test/conf' # Optional - #mergeTestResults: false # Optional - #failTaskOnFailedTests: false # Optional - #testRunTitle: # Optional - #buildPlatform: # Optional - #buildConfiguration: # Optional - #publishRunAttachments: true # Optional - displayName: 'Publish test results' diff --git a/pipeline/steps/init-gcloud-steps.yaml b/pipeline/steps/init-gcloud-steps.yaml deleted file mode 100644 index 71831e7ce..000000000 --- a/pipeline/steps/init-gcloud-steps.yaml +++ /dev/null @@ -1,22 +0,0 @@ -parameters: - windows: 'false' - -steps: -- task: DownloadSecureFile@1 - displayName: 'Download secure file' - inputs: - secureFile: 'azure-gcp.json' -- bash: | - keyFilePath="$(Agent.TempDirectory)/azure-gcp.json" - if [[ ${{ parameters.windows }} == "true" ]]; then - keyFilePath=$(wslpath "${keyFilePath}") - fi - if [[ -z $(which gcloud) ]]; then - CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" - echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list - curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - - sudo apt-get update && sudo apt-get install -y google-cloud-sdk - fi - gcloud --quiet auth activate-service-account --key-file="${keyFilePath}" - gcloud --quiet config set project $(gcp.project.name) - displayName: 'set up gcloud' \ No newline at end of file diff --git a/pipeline/steps/init-ssh.yaml b/pipeline/steps/init-ssh.yaml deleted file mode 100644 index 6b9b88f1a..000000000 --- a/pipeline/steps/init-ssh.yaml +++ /dev/null @@ -1,10 +0,0 @@ -steps: -- task: InstallSSHKey@0 - inputs: - knownHostsEntry: $(ssh.github.knownhost) - sshPublicKey: $(ssh.user.pub) - sshKeySecureFile: id_rsa -- task: DownloadSecureFile@1 - displayName: 'Download SSH keys to' - inputs: - secureFile: 'id_rsa' \ No newline at end of file diff --git a/pipeline/steps/init-vms.yaml b/pipeline/steps/init-vms.yaml deleted file mode 100644 index 8a6d7dc56..000000000 --- a/pipeline/steps/init-vms.yaml +++ /dev/null @@ -1,83 +0,0 @@ -parameters: - id: '' - distro: '' - repo: '' - agent_count: 1 - controller_count: 1 - windows: 'false' - -steps: -- template: init-gcloud-steps.yaml - parameters: - windows: ${{ parameters.windows }} -- bash: | - id=${{ parameters.id }} - distro=${{ parameters.distro }} - repo=${{ parameters.repo }} - agent_count=${{ parameters.agent_count }} - controller_count=${{ parameters.controller_count }} - agent_list="" - jobs=0 - - echo "vms: $distro $repo" - - for idx in $(seq 1 $agent_count); do - gcloud compute --project=$(gcp.project.name) instances create iofogctl-ci-$id-$idx --zone=$(gcp.vm.zone) --machine-type=n1-standard-1 --subnet=default --network-tier=PREMIUM --maintenance-policy=MIGRATE --service-account=$(gcp.svcacc.name) --scopes=https://www.googleapis.com/auth/devstorage.read_only,https://www.googleapis.com/auth/logging.write,https://www.googleapis.com/auth/monitoring.write,https://www.googleapis.com/auth/servicecontrol,https://www.googleapis.com/auth/service.management.readonly,https://www.googleapis.com/auth/trace.append --image=$distro --image-project=$repo --boot-disk-size=200GB --boot-disk-type=pd-standard --boot-disk-device-name=iofogctl-ci-$id-$idx & - ((jobs++)) - done - if [ $controller_count -gt 0 ]; then - idx=$((agent_count+1)) - gcloud compute --project=$(gcp.project.name) instances create iofogctl-ci-$id-$idx --zone=$(gcp.vm.zone) --machine-type=n1-standard-1 --subnet=default --network-tier=PREMIUM --maintenance-policy=MIGRATE --service-account=$(gcp.svcacc.name) --scopes=https://www.googleapis.com/auth/devstorage.read_only,https://www.googleapis.com/auth/logging.write,https://www.googleapis.com/auth/monitoring.write,https://www.googleapis.com/auth/servicecontrol,https://www.googleapis.com/auth/service.management.readonly,https://www.googleapis.com/auth/trace.append --image=$distro --image-project=$repo --boot-disk-size=200GB --boot-disk-type=pd-standard --boot-disk-device-name=iofogctl-ci-$id-$idx & - ((jobs++)) - fi - - for job in $(seq -s ' ' 1 $jobs); do - wait %$job - done - - for idx in $(seq 1 $agent_count); do - vm_host=$(gcloud compute instances list | grep iofogctl-ci-$id-$idx | awk '{print $5}') - agent_list="$(gcp.vm.user)@$vm_host $agent_list" - done - agent_list=$(echo "$agent_list" | awk '{$1=$1;print}') - echo "##vso[task.setvariable variable=agent_vm_list]$agent_list" - if [ $controller_count -gt 0 ]; then - idx=$((agent_count+1)) - vm_host=$(gcloud compute instances list | grep iofogctl-ci-$id-$idx | awk '{print $5}') - echo "##vso[task.setvariable variable=controller_vm]$(gcp.vm.user)@$vm_host" - fi - displayName: 'Deploy Test VMs' -- bash: | - controller_count=${{ parameters.controller_count }} - keyFilePath="$(Agent.TempDirectory)/id_rsa" - if [[ ${{ parameters.windows }} == "true" ]]; then - keyFilePath=$(wslpath "${keyFilePath}") - fi - cat $keyFilePath > /tmp/id_rsa - chmod 400 /tmp/id_rsa - cssh='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/id_rsa' - for agent in $(agent_vm_list); do - echo "Waiting for VM $agent" - iter=0 - while ! $cssh $agent -- echo "SSH success" && [ $iter -lt 15 ]; do - ((iter++)) - sleep 5 - done - if [ ! $iter -lt 15 ]; then - echo "Timed out waiting for $agent" - exit 1 - fi - done - if [ $controller_count -gt 0 ]; then - echo "Waiting for VM $(controller_vm)" - iter=0 - while ! $cssh $(controller_vm) -- echo "SSH success" && [ $iter -lt 15 ]; do - ((iter++)) - sleep 5 - done - if [ ! $iter -lt 15 ]; then - echo "Timed out waiting for $(controller_vm)" - exit 1 - fi - fi - displayName: 'Wait for VM SSH access' \ No newline at end of file diff --git a/pipeline/steps/install-test-deps.yaml b/pipeline/steps/install-test-deps.yaml deleted file mode 100644 index 6f833a199..000000000 --- a/pipeline/steps/install-test-deps.yaml +++ /dev/null @@ -1,5 +0,0 @@ -steps: -- bash: | - git clone https://github.com/bats-core/bats-core.git && cd bats-core && git checkout tags/v1.1.0 && sudo ./install.sh /usr/local - sudo npm i -g tap-junit - displayName: 'Install Test Deps' \ No newline at end of file diff --git a/pipeline/steps/postinstall.yaml b/pipeline/steps/postinstall.yaml deleted file mode 100644 index f22f93565..000000000 --- a/pipeline/steps/postinstall.yaml +++ /dev/null @@ -1,5 +0,0 @@ -steps: -- script: | - which iofogctl - iofogctl version - displayName: 'Verify Install' \ No newline at end of file diff --git a/pipeline/steps/prebuild.yaml b/pipeline/steps/prebuild.yaml deleted file mode 100644 index c4114ace2..000000000 --- a/pipeline/steps/prebuild.yaml +++ /dev/null @@ -1,26 +0,0 @@ -steps: -- script: | - set -e - mkdir -p '$(GOBIN)' - mkdir -p '$(GOPATH)/pkg' - echo '##vso[task.prependpath]$(GOBIN)' - echo '##vso[task.prependpath]$(GOROOT)/bin' - displayName: 'Set up the Go workspace' -- task: GoTool@0 - inputs: - version: '1.17.9' - goPath: $(GOPATH) - goBin: $(GOBIN) - displayName: 'Install Golang' -- script: | - set -e - script/check_fmt.sh - displayName: 'Check Source Format' -- script: | - set -e - PIPELINE=1 script/bootstrap.sh - displayName: 'Bootstrap' -- script: | - set -e - make test - displayName: 'Run Unit Tests' \ No newline at end of file diff --git a/pipeline/steps/publish-deps.yaml b/pipeline/steps/publish-deps.yaml deleted file mode 100644 index b2cde9543..000000000 --- a/pipeline/steps/publish-deps.yaml +++ /dev/null @@ -1,18 +0,0 @@ -steps: -- task: UseRubyVersion@0 - inputs: - versionSpec: '2.7' - addToPath: true - displayName: 'Install Ruby' -- task: DownloadSecureFile@1 - inputs: - secureFile: 'package_cloud' - displayName: 'Download package cloud token file' -- script: | - gem install fpm - fpm -h - gem install package_cloud - package_cloud -h - echo "config file..." - echo $(Agent.TempDirectory)/package_cloud - displayName: 'Install package_cloud cli and fpm' \ No newline at end of file diff --git a/pipeline/steps/vanilla.yaml b/pipeline/steps/vanilla.yaml deleted file mode 100644 index 9c9c65f05..000000000 --- a/pipeline/steps/vanilla.yaml +++ /dev/null @@ -1,44 +0,0 @@ -parameters: - id: '' - distro: '' - repo: '' - agent_count: 1 - controller_count: 1 - -steps: -- task: DownloadBuildArtifacts@0 - displayName: 'Download Build Artifacts' - inputs: - artifactName: iofogctl - downloadPath: $(System.DefaultWorkingDirectory) -- script: | - sudo cp iofogctl/build_linux_linux_amd64/iofogctl /usr/local/bin/ - sudo chmod 0755 /usr/local/bin/iofogctl -- template: postinstall.yaml -- template: init-ssh.yaml -- template: init-vms.yaml - parameters: - id: ${{ parameters.id }} - distro: ${{ parameters.distro }} - repo: ${{ parameters.repo }} - agent_count: ${{ parameters.agent_count }} - controller_count: ${{ parameters.controller_count }} -- template: configure-remote-tests.yaml -- template: install-test-deps.yaml -- script: | - test/run.bash smoke - displayName: 'Run Smoke Tests' -- script: | - set -o pipefail - test/run.bash vanilla | tee test/conf/results-vanilla.tap - displayName: 'Run Functional Tests' -- script: | - tap-junit -i test/conf/results-vanilla.tap -o test/conf -s Vanilla -n results-vanilla.xml || true - displayName: 'Convert test output from TAP to JUnit' - condition: succeededOrFailed() -- template: functional-post-test.yaml -- template: functional-clean-vm.yaml - parameters: - id: ${{ parameters.id }} - agent_count: ${{ parameters.agent_count }} - controller_count: ${{ parameters.controller_count }} diff --git a/pipeline/steps/version.yaml b/pipeline/steps/version.yaml deleted file mode 100644 index a418fa048..000000000 --- a/pipeline/steps/version.yaml +++ /dev/null @@ -1,15 +0,0 @@ -steps: -- script: | - . version.sh - VERS=$MAJOR.$MINOR.$PATCH$SUFFIX - echo "$VERS" - if [[ $(ref) == refs/tags* ]]; then - TAG=$(echo $(ref) | sed "s|refs/tags/v||g") - if [[ $TAG != $VERS ]]; then - echo 'Version file does not match git tag' - exit 1 - fi - fi - echo "##vso[task.setvariable variable=version]$VERS" - echo "Version: $VERS" - displayName: 'Set version variable' \ No newline at end of file diff --git a/pipeline/vanilla.yaml b/pipeline/vanilla.yaml deleted file mode 100644 index 70646f5f8..000000000 --- a/pipeline/vanilla.yaml +++ /dev/null @@ -1,20 +0,0 @@ -parameters: - job_name: '' - id: '' - distro: '' - repo: '' - agent_count: 1 - controller_count: 1 - -jobs: -- job: ${{ parameters.job_name }} - pool: - vmImage: 'Ubuntu-20.04' - steps: - - template: steps/vanilla.yaml - parameters: - id: $(jobuuid) - distro: ${{ parameters.distro }} - repo: ${{ parameters.repo }} - agent_count: 2 - controller_count: 1 \ No newline at end of file diff --git a/pipeline/win-k8s.yaml b/pipeline/win-k8s.yaml deleted file mode 100644 index 2716165ce..000000000 --- a/pipeline/win-k8s.yaml +++ /dev/null @@ -1,106 +0,0 @@ -jobs: -- job: Windows_K8s - pool: 'Azure Windows' - steps: - - bash: | - rm -rf /mnt/c/Users/$(azure.windows.user)/.iofog/ - displayName: 'Clean up Windows env' - - task: DownloadBuildArtifacts@0 - displayName: 'Download Build Artifacts' - inputs: - artifactName: iofogctl - downloadPath: $(System.DefaultWorkingDirectory) - - bash: | - dir=$(wslpath "C:\Users\$(azure.windows.user)\AppData\Local\Microsoft\WindowsApps") - echo moving - mv windows/iofogctl $dir/ - - echo chmodding - chmod +x $dir/iofogctl - - echo version - $dir/iofogctl version - iofogctl version - displayName: 'Prepare iofogctl binary' - - bash: | - tempBashPath=$(wslpath "$(Agent.TempDirectory)") - cd $tempBashPath - git clone https://github.com/bats-core/bats-core.git && cd bats-core && git checkout tags/v1.1.0 && ./install.sh /usr - bats --version - displayName: 'Install Bats' - - bash: | - for suffix in bash sh bats; do - for file in $(find ./test -name "*.$suffix"); do - dos2unix -o $file - done - done - for file in $(find ./assets/agent -name '*.sh'); do dos2unix -o $file; done - displayName: 'Format test files' - - template: steps/init-ssh.yaml - - template: steps/init-vms.yaml - parameters: - id: wink8s$(build) - distro: $(gcp.vm.distro.xenial) - repo: $(gcp.vm.repo.ubuntu) - agent_count: 2 - controller_count: 0 - windows: 'true' - - template: steps/configure-remote-tests.yaml - parameters: - windows: 'true' - - task: DownloadSecureFile@1 - displayName: 'Download SSH keys to' - name: 'gcp_iofogctl_rsa' - inputs: - secureFile: 'gcp_iofogctl_rsa' - - bash: | - destFolder=$(wslpath "$(windows_ssh_key_path)") - echo "SSH downloaded at $(gcp_iofogctl_rsa.secureFilePath)" - echo "Converting windows path to bash path" - bashPath=$(wslpath "$(gcp_iofogctl_rsa.secureFilePath)") - echo "Bash path = $bashPath" - ls $bashPath - mkdir -p $destFolder - cp $bashPath $destFolder/$(ssh_key_file) - echo "Copied SSH fey from $bashPath to $destFolder" - chmod 0700 $destFolder - chmod 0600 $destFolder/$(ssh_key_file) - echo '' > $destFolder/known_hosts - ls -la $destFolder - displayName: Prepare SSH key - - bash: | - sudo apt-get install -y jq - displayName: 'Install jq' - - bash: | - sed -i "s|KEY_FILE=.*|KEY_FILE=\"$(windows_ssh_key_path)/$(ssh_key_file)\"|g" test/conf/env.sh - sed -i "s|KUBE_CONFIG=.*|KUBE_CONFIG=\"$(windows_kube_config_path)\"|g" test/conf/env.sh - sed -i "s|TEST_KUBE_CONFIG=.*|TEST_KUBE_CONFIG=\"$(bash_kube_config_path)\"|g" test/conf/env.sh - cat test/conf/env.sh - displayName: 'Prepare Test Config' - - bash: | - kubePath=$(wslpath "C:\Users\$(azure.windows.user)\.kube\config") - export KUBECONFIG="$kubePath" - gcloud --quiet container clusters get-credentials $(gcp.cluster.name) --region $(gcp.cluster.region) - gcloudPath="C:\\\Program Files (x86)\\\Google\\\Cloud SDK\\\google-cloud-sdk\\\bin\\\gcloud" - sed -i "s|cmd-path:.*|cmd-path: $gcloudPath|g" $kubePath - displayName: 'Connect to cluster' - - bash: | - set -o pipefail - export WSL_KEY_FILE=$(wslpath "$(windows_ssh_key_path)/$(ssh_key_file)") - echo $WSL_KEY_FILE - test/run.bash k8s | tee test/conf/results-k8s.tap - displayName: 'Run Functional Tests' - - bash: | - tap-junit -i test/conf/results-k8s.tap -o test/conf -s K8s -n results-k8s.xml || true - displayName: 'Convert test output from TAP to JUnit' - condition: succeededOrFailed() - - bash: | - test/clean.bash - displayName: 'Clean K8s Cluster' - condition: always() - - template: steps/functional-post-test.yaml - - template: steps/functional-clean-vm.yaml - parameters: - id: wink8s$(build) - agent_count: 2 - controller_count: 0 \ No newline at end of file diff --git a/pipeline/win-local.yaml b/pipeline/win-local.yaml deleted file mode 100644 index 382cd170f..000000000 --- a/pipeline/win-local.yaml +++ /dev/null @@ -1,104 +0,0 @@ -jobs: -- job: Windows_Local - pool: 'Azure Windows' - steps: - - bash: | - rm -rf /mnt/c/Users/$(azure.windows.user)/.iofog/ - displayName: 'Clean up Windows env' - - task: DownloadBuildArtifacts@0 - displayName: 'Download Build Artifacts' - inputs: - artifactName: windows - downloadPath: $(System.DefaultWorkingDirectory) - - bash: | - dir=/mnt/c/Users/$(azure.windows.user)/AppData/Local/Microsoft/WindowsApps/ - echo moving - mv windows/iofogctl $dir - - echo chmodding - chmod +x $dir/iofogctl - - echo version - $dir/iofogctl version - iofogctl version - displayName: 'Prepare iofogctl binary' - - bash: | - if [[ -z $(which docker) ]]; then - apt-get update -y - apt-get install -y \ - apt-transport-https \ - ca-certificates \ - curl \ - software-properties-common - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - add-apt-repository \ - "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ - $(lsb_release -cs) \ - stable" - apt-get update -y - apt-get install -y docker-ce - usermod -aG docker $USER - fi - displayName: Install docker if necessary - - template: steps/init-gcloud-steps.yaml - parameters: - windows: "true" - - bash: | - if [[ -z $(echo $DOCKER_HOST) ]]; then - export DOCKER_HOST="tcp://localhost:2375" - fi - gcloud --quiet auth configure-docker - echo "$DOCKER_HOST" - docker info - docker "pull" "$(controller_image)" - docker "pull" "$(agent_image)" - displayName: 'Pull develop gcr docker image' - failOnStderr: false - - template: steps/configure-remote-tests.yaml - - bash: | - tempBashPath=$(wslpath "$(Agent.TempDirectory)") - cd $tempBashPath - git clone https://github.com/bats-core/bats-core.git && cd bats-core && git checkout tags/v1.1.0 && ./install.sh /usr - bats --version - displayName: 'Install Bats' - - bash: | - for file in $(find ./test -name '*.bash'); do dos2unix -o $file; done - for file in $(find ./test -name '*.sh'); do dos2unix -o $file; done - for file in $(find ./test -name '*.bats'); do dos2unix -o $file; done - displayName: 'Format test files' - - bash: | - if [[ -z $(echo $DOCKER_HOST) ]]; then - export DOCKER_HOST="tcp://localhost:2375" - fi - set -o pipefail - echo "$DOCKER_HOST" - docker images - export WSL_KEY_FILE=$(wslpath "$(windows_ssh_key_path)/$(ssh_key_file)") - echo $WSL_KEY_FILE - test/run.bash local | tee test/conf/results-local.tap - displayName: 'Run Functional Tests' - - script: | - RD /S /Q "C:\Users\$(azure.windows.user)\.iofog\" - condition: always() - displayName: 'Clean local .iofog environment' - - bash: | - if [[ -z $(echo $DOCKER_HOST) ]]; then - export DOCKER_HOST="tcp://localhost:2375" - fi - docker rm -f $(docker ps -aq) - docker "system" "prune" "-af" - condition: always() - displayName: 'Clean local docker environment' - -- job: Vanilla_Xenial - condition: and(succeeded(), startsWith(variables['build.sourceBranch'], 'refs/tags/')) - pool: - vmImage: 'Ubuntu-18.04' - steps: - - template: steps/vanilla.yaml - parameters: - id: $(jobuuid) - distro: $(gcp.vm.distro.xenial) - repo: $(gcp.vm.repo.ubuntu) - agent_count: 2 - controller_count: 1 \ No newline at end of file diff --git a/pipeline/win-vanilla.yaml b/pipeline/win-vanilla.yaml deleted file mode 100644 index 9e4492f57..000000000 --- a/pipeline/win-vanilla.yaml +++ /dev/null @@ -1,88 +0,0 @@ -jobs: -- job: Windows_Vanilla - pool: 'Azure Windows' - steps: - - bash: | - rm -rf /mnt/c/Users/$(azure.windows.user)/.iofog/ - displayName: 'Clean up Windows env' - - template: steps/init-ssh.yaml - - template: steps/init-vms.yaml - parameters: - id: win$(build) - distro: $(gcp.vm.distro.buster) - repo: $(gcp.vm.repo.debian) - agent_count: 2 - controller_count: 1 - windows: "true" - - template: steps/configure-remote-tests.yaml - parameters: - windows: 'true' - - task: DownloadSecureFile@1 - displayName: 'Download SSH keys to' - name: 'gcp_iofogctl_rsa' - inputs: - secureFile: 'gcp_iofogctl_rsa' - - bash: | - destFolder=$(wslpath "$(windows_ssh_key_path)") - echo "SSH downloaded at $(gcp_iofogctl_rsa.secureFilePath)" - echo "Converting windows path to bash path" - bashPath=$(wslpath "$(gcp_iofogctl_rsa.secureFilePath)") - echo "Bash path = $bashPath" - ls $bashPath - mkdir -p $destFolder - cp $bashPath $destFolder/$(ssh_key_file) - echo "Copied SSH key from $bashPath to $destFolder" - chmod 0700 $destFolder - chmod 0600 $destFolder/$(ssh_key_file) - echo '' > $destFolder/known_hosts - ls -la $destFolder - displayName: Prepare SSH key - - bash: | - sed -i "s|KEY_FILE=.*|KEY_FILE=\"$(windows_ssh_key_path)/$(ssh_key_file)\"|g" test/conf/env.sh - cat test/conf/env.sh - displayName: 'Prepare Test Config' - - task: DownloadBuildArtifacts@0 - displayName: 'Download Build Artifacts' - inputs: - artifactName: windows - downloadPath: $(System.DefaultWorkingDirectory) - - bash: | - dir=$(wslpath "C:\Users\$(azure.windows.user)\AppData\Local\Microsoft\WindowsApps") - echo moving - mv windows/iofogctl $dir/ - - echo chmodding - chmod +x $dir/iofogctl - - echo version - $dir/iofogctl version - iofogctl version - displayName: 'Prepare iofogctl binary' - - bash: | - tempBashPath=$(wslpath "$(Agent.TempDirectory)") - cd $tempBashPath - git clone https://github.com/bats-core/bats-core.git && cd bats-core && git checkout tags/v1.1.0 && ./install.sh /usr - bats --version - displayName: 'Install Bats' - - bash: | - sudo apt-get install -y jq - displayName: 'Install jq' - - bash: | - for file in $(find ./test -name '*.bash'); do dos2unix -o $file; done - for file in $(find ./test -name '*.sh'); do dos2unix -o $file; done - for file in $(find ./test -name '*.bats'); do dos2unix -o $file; done - for file in $(find ./assets/agent -name '*.sh'); do dos2unix -o $file; done - displayName: 'Format test files' - - bash: | - test/run.bash smoke - displayName: 'Run Smoke Tests' - - bash: | - export WSL_KEY_FILE=$(wslpath "$(windows_ssh_key_path)/$(ssh_key_file)") - echo $WSL_KEY_FILE - test/run.bash vanilla - displayName: 'Run Functional Tests' - - template: steps/functional-clean-vm.yaml - parameters: - id: win$(build) - agent_count: 2 - controller_count: 1 \ No newline at end of file diff --git a/pkg/containerengine/docker/client.go b/pkg/containerengine/docker/client.go new file mode 100644 index 000000000..e50bdde7b --- /dev/null +++ b/pkg/containerengine/docker/client.go @@ -0,0 +1,44 @@ +// Package docker wraps the Moby API for local edgelet container operations. +// TODO(v3.8.0): remove after local and remote control plane no longer use Go container deploy. +package docker + +import ( + "context" + "fmt" + "strings" + + mobyclient "github.com/moby/moby/client" +) + +// Client wraps the Moby Docker API client. +type Client struct { + cli *mobyclient.Client +} + +// NewWithHost connects to a container engine using a unix/tcp host URL. +// When hostURL is empty, Moby falls back to DOCKER_HOST and default socket discovery. +func NewWithHost(hostURL string) (*Client, error) { + var opts []mobyclient.Opt + if strings.TrimSpace(hostURL) != "" { + opts = append(opts, mobyclient.WithHost(hostURL)) + } else { + opts = append(opts, mobyclient.FromEnv) + } + cli, err := mobyclient.New(opts...) + if err != nil { + return nil, fmt.Errorf("docker client: %w", err) + } + return &Client{cli: cli}, nil +} + +// Close releases the underlying client. +func (c *Client) Close() error { + if c == nil || c.cli == nil { + return nil + } + return c.cli.Close() +} + +func (c *Client) context() context.Context { + return context.Background() +} diff --git a/pkg/containerengine/docker/container.go b/pkg/containerengine/docker/container.go new file mode 100644 index 000000000..0520b16e1 --- /dev/null +++ b/pkg/containerengine/docker/container.go @@ -0,0 +1,292 @@ +package docker + +import ( + "bytes" + "errors" + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/moby/moby/api/pkg/stdcopy" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" + mobyclient "github.com/moby/moby/client" +) + +const ( + RestartPolicyAlways = "always" + NetworkModeHost = "host" +) + +// ContainerSummary is a minimal container listing entry. +type ContainerSummary struct { + ID string + Names []string + Image string + Status string + State string +} + +// DeployOptions configures container create/start. +type DeployOptions struct { + Name string + Image string + Env []string + Binds []string + Privileged bool + NetworkMode string + PortBindings map[string]PortBinding // hostPort -> container port/proto + RestartPolicy string + StopTimeout time.Duration +} + +// PortBinding maps a container port to a host port. +type PortBinding struct { + HostIP string + HostPort string + ContainerPort string + Protocol string +} + +// ExecResult holds output from a container exec. +type ExecResult struct { + StdOut string + StdErr string + ExitCode int +} + +// ListContainers returns containers; when all is false, only running containers are returned. +func (c *Client) ListContainers(all bool) ([]ContainerSummary, error) { + result, err := c.cli.ContainerList(c.context(), mobyclient.ContainerListOptions{All: all}) + if err != nil { + return nil, err + } + out := make([]ContainerSummary, 0, len(result.Items)) + for _, item := range result.Items { + out = append(out, ContainerSummary{ + ID: item.ID, + Names: item.Names, + Image: item.Image, + Status: item.Status, + State: string(item.State), + }) + } + return out, nil +} + +// GetContainerByName finds a container by exact name (with or without leading slash). +func (c *Client) GetContainerByName(name string) (ContainerSummary, error) { + containers, err := c.ListContainers(true) + if err != nil { + return ContainerSummary{}, err + } + target := normalizeContainerName(name) + for _, cont := range containers { + for _, containerName := range cont.Names { + if normalizeContainerName(containerName) == target { + return cont, nil + } + } + } + return ContainerSummary{}, util.NewInputError(fmt.Sprintf("Could not find container %s", name)) +} + +func normalizeContainerName(name string) string { + return strings.TrimPrefix(strings.TrimSpace(name), "/") +} + +// RemoveContainerByName stops and force-removes a container if it exists. +func (c *Client) RemoveContainerByName(name string) error { + cont, err := c.GetContainerByName(name) + if err != nil { + var inputErr *util.InputError + if errors.As(err, &inputErr) { + return nil + } + return err + } + return c.RemoveContainerByID(cont.ID) +} + +// RemoveContainerByID stops and force-removes a container by ID. +func (c *Client) RemoveContainerByID(id string) error { + ctx := c.context() + timeout := 60 + _, _ = c.cli.ContainerStop(ctx, id, mobyclient.ContainerStopOptions{Timeout: &timeout}) + _, err := c.cli.ContainerRemove(ctx, id, mobyclient.ContainerRemoveOptions{Force: true}) + return err +} + +// DeployContainer pulls (when needed), recreates, and starts a container. +func (c *Client) DeployContainer(opts DeployOptions, pullOpts PullOptions) (string, error) { + if err := c.RemoveContainerByName(opts.Name); err != nil { + return "", err + } + if err := c.PullImage(opts.Image, pullOpts); err != nil { + return "", err + } + + portSet := network.PortSet{} + portMap := network.PortMap{} + for hostPort, binding := range opts.PortBindings { + proto := network.TCP + if strings.EqualFold(binding.Protocol, "udp") { + proto = network.UDP + } + containerPort := binding.ContainerPort + if containerPort == "" { + containerPort = hostPort + } + portNum, err := strconv.ParseUint(containerPort, 10, 16) + if err != nil { + return "", fmt.Errorf("invalid container port %q: %w", containerPort, err) + } + natPort, ok := network.PortFrom(uint16(portNum), proto) + if !ok { + return "", fmt.Errorf("invalid container port %q", containerPort) + } + portSet[natPort] = struct{}{} + hostBindingPort := binding.HostPort + if hostBindingPort == "" { + hostBindingPort = hostPort + } + portMap[natPort] = []network.PortBinding{{HostPort: hostBindingPort}} + } + + networkMode := container.NetworkMode(opts.NetworkMode) + restartName := container.RestartPolicyAlways + if opts.RestartPolicy != "" { + restartName = container.RestartPolicyMode(opts.RestartPolicy) + } + restartPolicy := container.RestartPolicy{Name: restartName} + + hostConfig := &container.HostConfig{ + Binds: opts.Binds, + Privileged: opts.Privileged, + NetworkMode: networkMode, + PortBindings: portMap, + RestartPolicy: restartPolicy, + } + + containerConfig := &container.Config{ + Image: opts.Image, + Env: opts.Env, + ExposedPorts: portSet, + } + if stopTimeout := int(opts.StopTimeout.Seconds()); stopTimeout > 0 { + containerConfig.StopTimeout = &stopTimeout + } + + createResp, err := c.cli.ContainerCreate(c.context(), mobyclient.ContainerCreateOptions{ + Name: opts.Name, + Config: containerConfig, + HostConfig: hostConfig, + }) + if err != nil { + return "", util.NewError(fmt.Sprintf("Failed to create container: %v", err)) + } + + _, err = c.cli.ContainerStart(c.context(), createResp.ID, mobyclient.ContainerStartOptions{}) + if err != nil { + return "", util.NewError(fmt.Sprintf("Failed to start container: %v", err)) + } + return createResp.ID, nil +} + +// GetContainerIP returns the primary IPv4 address for a container. +func (c *Client) GetContainerIP(name string) (string, error) { + cont, err := c.GetContainerByName(name) + if err != nil { + return "", err + } + inspect, err := c.cli.ContainerInspect(c.context(), cont.ID, mobyclient.ContainerInspectOptions{}) + if err != nil { + return "", err + } + cfg := inspect.Container + if cfg.HostConfig != nil && cfg.HostConfig.NetworkMode == NetworkModeHost { + return "127.0.0.1", nil + } + if cfg.NetworkSettings == nil { + return "", util.NewNotFoundError(fmt.Sprintf("Container %s: no network settings", name)) + } + mode := "bridge" + if cfg.HostConfig != nil && cfg.HostConfig.NetworkMode != "" { + mode = string(cfg.HostConfig.NetworkMode) + } + if net, ok := cfg.NetworkSettings.Networks[mode]; ok && net.IPAddress.IsValid() { + return net.IPAddress.String(), nil + } + for _, candidate := range cfg.NetworkSettings.Networks { + if candidate != nil && candidate.IPAddress.IsValid() { + return candidate.IPAddress.String(), nil + } + } + return "", util.NewNotFoundError(fmt.Sprintf("Container %s: could not find network setting for network %s", name, mode)) +} + +// GetLogsByName returns stdout and stderr log streams for a container. +func (c *Client) GetLogsByName(name string) (stdout, stderr string, err error) { + cont, err := c.GetContainerByName(name) + if err != nil { + return "", "", err + } + logs, err := c.cli.ContainerLogs(c.context(), cont.ID, mobyclient.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + }) + if err != nil { + return "", "", err + } + defer logs.Close() + + stdoutBuf := new(bytes.Buffer) + stderrBuf := new(bytes.Buffer) + if _, err = stdcopy.StdCopy(stdoutBuf, stderrBuf, logs); err != nil { + return "", "", err + } + return stdoutBuf.String(), stderrBuf.String(), nil +} + +// ExecuteCmd runs a command inside a container and returns its output. +func (c *Client) ExecuteCmd(name string, cmd []string) (ExecResult, error) { + var result ExecResult + cont, err := c.GetContainerByName(name) + if err != nil { + return result, err + } + + ctx := c.context() + execResp, err := c.cli.ExecCreate(ctx, cont.ID, mobyclient.ExecCreateOptions{ + Cmd: cmd, + AttachStdout: true, + AttachStderr: true, + }) + if err != nil { + return result, err + } + + attachResp, err := c.cli.ExecAttach(ctx, execResp.ID, mobyclient.ExecAttachOptions{}) + if err != nil { + return result, err + } + defer attachResp.Close() + + var outBuf, errBuf bytes.Buffer + if _, err = stdcopy.StdCopy(&outBuf, &errBuf, attachResp.Reader); err != nil && !errors.Is(err, io.EOF) { + return result, err + } + + inspect, err := c.cli.ExecInspect(ctx, execResp.ID, mobyclient.ExecInspectOptions{}) + if err != nil { + return result, err + } + + result.ExitCode = inspect.ExitCode + result.StdOut = outBuf.String() + result.StdErr = errBuf.String() + return result, nil +} diff --git a/pkg/containerengine/docker/copy.go b/pkg/containerengine/docker/copy.go new file mode 100644 index 000000000..c09dfdc71 --- /dev/null +++ b/pkg/containerengine/docker/copy.go @@ -0,0 +1,102 @@ +package docker + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "time" + + mobyclient "github.com/moby/moby/client" +) + +// CopyToContainer archives a host directory and extracts it inside a container path. +func (c *Client) CopyToContainer(name, source, dest string) error { + cont, err := c.GetContainerByName(name) + if err != nil { + return err + } + var content bytes.Buffer + if err := compressDir(source, &content); err != nil { + return err + } + if _, err := c.ExecuteCmd(name, []string{"mkdir", "-p", dest}); err != nil { + return err + } + _, err = c.cli.CopyToContainer(c.context(), cont.ID, mobyclient.CopyToContainerOptions{ + DestinationPath: dest, + Content: &content, + }) + return err +} + +func compressDir(src string, buf io.Writer) error { + src = filepath.Clean(src) + root, err := os.OpenRoot(src) + if err != nil { + return err + } + defer root.Close() + + zr := gzip.NewWriter(buf) + tw := tar.NewWriter(zr) + + err = fs.WalkDir(root.FS(), ".", func(rel string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if rel == "." { + return nil + } + info, err := entry.Info() + if err != nil { + return err + } + header, err := tar.FileInfoHeader(info, rel) + if err != nil { + return err + } + header.Name = filepath.ToSlash(rel) + if err := tw.WriteHeader(header); err != nil { + return err + } + if info.IsDir() { + return nil + } + f, err := root.Open(rel) + if err != nil { + return err + } + defer f.Close() + if _, err := io.Copy(tw, f); err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + if err := tw.Close(); err != nil { + return err + } + if err := zr.Close(); err != nil { + return err + } + return nil +} + +// WaitForCommand polls a container command until output matches condition. +func (c *Client) WaitForCommand(containerName string, match func(stdout string) bool, command ...string) error { + for iteration := 0; iteration < 120; iteration++ { + output, err := c.ExecuteCmd(containerName, command) + if err == nil && match(output.StdOut) { + return nil + } + time.Sleep(2 * time.Second) + } + return fmt.Errorf("timed out waiting for container command %v", command) +} diff --git a/pkg/containerengine/docker/image.go b/pkg/containerengine/docker/image.go new file mode 100644 index 000000000..522374618 --- /dev/null +++ b/pkg/containerengine/docker/image.go @@ -0,0 +1,90 @@ +package docker + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/eclipse-iofog/iofogctl/pkg/util" + dockerregistry "github.com/moby/moby/api/types/registry" + mobyclient "github.com/moby/moby/client" +) + +// PullOptions configures image pull behavior. +type PullOptions struct { + Username string + Password string +} + +// PullImage pulls an image or verifies it exists locally when pull fails. +func (c *Client) PullImage(image string, opts PullOptions) error { + ctx := c.context() + pullOpts := mobyclient.ImagePullOptions{} + if opts.Username != "" { + authConfig := dockerregistry.AuthConfig{ + Username: opts.Username, + Password: opts.Password, + } + authJSON, err := json.Marshal(authConfig) // #nosec G117 -- Moby registry auth JSON required by Docker API + if err != nil { + return err + } + pullOpts.RegistryAuth = base64.URLEncoding.EncodeToString(authJSON) + } + + reader, err := c.cli.ImagePull(ctx, image, pullOpts) + if err != nil { + if found, listErr := c.imageExistsLocally(image); listErr != nil { + return listErr + } else if found { + return nil + } + return err + } + defer reader.Close() + if _, err := io.Copy(io.Discard, reader); err != nil { + return err + } + return waitForLocalImage(c, normalizeImageTag(image), 0) +} + +func (c *Client) imageExistsLocally(image string) (bool, error) { + images, err := c.cli.ImageList(c.context(), mobyclient.ImageListOptions{All: true}) + if err != nil { + return false, err + } + target := normalizeImageTag(image) + for _, img := range images.Items { + for _, tag := range img.RepoTags { + if tag == target || tag == image { + return true, nil + } + } + } + return false, nil +} + +func normalizeImageTag(image string) string { + if strings.HasPrefix(image, "docker.io/") { + return image[len("docker.io/"):] + } + return image +} + +func waitForLocalImage(c *Client, image string, attempt int8) error { + if attempt >= 18 { + return util.NewInternalError("Could not find newly pulled image: " + image) + } + found, err := c.imageExistsLocally(image) + if err != nil { + return util.NewError(fmt.Sprintf("Could not list local images: %v", err)) + } + if found { + return nil + } + time.Sleep(10 * time.Second) + return waitForLocalImage(c, image, attempt+1) +} diff --git a/pkg/iofog/constants.go b/pkg/iofog/constants.go index 7a342e824..024bca98d 100644 --- a/pkg/iofog/constants.go +++ b/pkg/iofog/constants.go @@ -1,21 +1,8 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package iofog import "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" -// String and numeric values of TCP ports used accross ioFog +// String and numeric values of TCP ports used across ioFog const ( ControllerPort = client.ControllerPort ControllerPortString = client.ControllerPortString diff --git a/pkg/iofog/install/agent.go b/pkg/iofog/install/agent.go index a7333d88f..c2abef7d0 100644 --- a/pkg/iofog/install/agent.go +++ b/pkg/iofog/install/agent.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package install import ( @@ -20,24 +7,25 @@ import ( type Agent interface { Bootstrap() error - getProvisionKey(string, IofogUser) (string, string, string, error) + getProvisionKey(controllerEndpoint string, user IofogUser, sdkOpt client.Options) (string, string, string, error) } +// getProvisionKeyHook is set by tests to avoid Controller API calls during provision tests. +var getProvisionKeyHook func(agent *defaultAgent, controllerEndpoint string, user IofogUser, sdkOpt client.Options) (string, string, error) + // defaultAgent implements commong behavior type defaultAgent struct { name string uuid string } -func (agent *defaultAgent) getProvisionKey(controllerEndpoint string, user IofogUser) (key string, caCert string, err error) { - // Connect to controller - baseURL, err := util.GetBaseURL(controllerEndpoint) - if err != nil { - return +func (agent *defaultAgent) getProvisionKey(controllerEndpoint string, user IofogUser, sdkOpt client.Options) (key string, caCert string, err error) { + if getProvisionKeyHook != nil { + return getProvisionKeyHook(agent, controllerEndpoint, user, sdkOpt) } // Log in util.SpinHandlePrompt() - ctrl, err := client.SessionLogin(client.Options{BaseURL: baseURL}, user.RefreshToken, user.Email, user.Password) + ctrl, err := client.SessionLogin(sdkOpt, user.RefreshToken, user.Email, user.Password) if err != nil { return } diff --git a/pkg/iofog/install/container_engine.go b/pkg/iofog/install/container_engine.go new file mode 100644 index 000000000..adb071c07 --- /dev/null +++ b/pkg/iofog/install/container_engine.go @@ -0,0 +1,42 @@ +package install + +import ( + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + // TODO(v3.8.0): remove Moby client after local and remote control plane no longer use Go container deploy. + dockengine "github.com/eclipse-iofog/iofogctl/pkg/containerengine/docker" +) + +// DefaultLocalContainerEngine is the host runtime used for local edgelet container operations. +const DefaultLocalContainerEngine = "docker" + +// DefaultContainerEngineURL returns the default unix socket URL for a container engine type. +func DefaultContainerEngineURL(engine string) string { + return defaultContainerEngineURL(engine) +} + +// ResolveContainerEngine returns the configured container engine name. +func ResolveContainerEngine(cfg *client.AgentConfiguration) string { + return resolveContainerEngine(cfg) +} + +// ResolveContainerEngineURL resolves the daemon socket URL from agent config and engine type. +func ResolveContainerEngineURL(engine string, cfg *client.AgentConfiguration) string { + return resolveContainerEngineURL(engine, cfg) +} + +// NewContainerEngineClient dials the container engine using ResolveContainerEngineURL. +func NewContainerEngineClient(engine string, agentCfg *client.AgentConfiguration) (*dockengine.Client, error) { + return dockengine.NewWithHost(ResolveContainerEngineURL(engine, agentCfg)) +} + +// LocalContainerEngineForHostOps maps agent config to the host runtime used for docker/podman API calls. +// Container-mode local agents use docker/podman; native edgelet-managed engines are not API-dial targets here. +func LocalContainerEngineForHostOps(agentCfg *client.AgentConfiguration) string { + engine := ResolveContainerEngine(agentCfg) + if strings.EqualFold(engine, "edgelet") { + return DefaultLocalContainerEngine + } + return engine +} diff --git a/pkg/iofog/install/container_engine_test.go b/pkg/iofog/install/container_engine_test.go new file mode 100644 index 000000000..a5076f223 --- /dev/null +++ b/pkg/iofog/install/container_engine_test.go @@ -0,0 +1,35 @@ +package install + +import ( + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" +) + +func TestDefaultContainerEngineURL(t *testing.T) { + if got := DefaultContainerEngineURL("docker"); got != "unix:///var/run/docker.sock" { + t.Fatalf("docker default = %q", got) + } +} + +func TestResolveContainerEngineURLPrefersAgentConfig(t *testing.T) { + custom := "unix:///custom/docker.sock" + cfg := &client.AgentConfiguration{ContainerEngineURL: strPtr(custom)} + if got := ResolveContainerEngineURL("docker", cfg); got != custom { + t.Fatalf("got %q want %q", got, custom) + } +} + +func TestLocalContainerEngineForHostOps(t *testing.T) { + if got := LocalContainerEngineForHostOps(nil); got != DefaultLocalContainerEngine { + t.Fatalf("nil cfg engine = %q", got) + } + cfg := &client.AgentConfiguration{ContainerEngine: strPtr("edgelet")} + if got := LocalContainerEngineForHostOps(cfg); got != DefaultLocalContainerEngine { + t.Fatalf("edgelet engine mapped to %q", got) + } + cfg.ContainerEngine = strPtr("podman") + if got := LocalContainerEngineForHostOps(cfg); got != "podman" { + t.Fatalf("podman engine = %q", got) + } +} diff --git a/pkg/iofog/install/controller.go b/pkg/iofog/install/controller.go index e696f79f4..0e74d1ddf 100644 --- a/pkg/iofog/install/controller.go +++ b/pkg/iofog/install/controller.go @@ -1,917 +1,125 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package install import ( - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - "time" + "errors" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/pkg/iofog" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -const ( - controllerAssetPrefixContainer = "container-controller" - controllerAssetPrefixAirgap = "airgap-controller" ) -type RemoteSystemImages struct { - ARM string `yaml:"arm,omitempty"` - X86 string `yaml:"x86,omitempty"` -} - -type RemoteSystemMicroservices struct { - Router RemoteSystemImages `yaml:"router,omitempty"` - Nats RemoteSystemImages `yaml:"nats,omitempty"` -} - -type ControllerOptions struct { - User string - Host string - Port int - Namespace string - PrivKeyFilename string - Version string - Image string - SystemMicroservices RemoteSystemMicroservices - NatsEnabled *bool // NATS enabling for remote control plane (nil = default enabled) - Vault *VaultConfig - PidBaseDir string - EcnViewerPort int - EcnViewerURL string - LogLevel string - Https *Https - SiteCA *SiteCertificate - LocalCA *SiteCertificate - Airgap bool -} - -type Https struct { - Enabled *bool - CACert string - TLSCert string - TLSKey string -} - -type SiteCertificate struct { - TLSCert string - TLSKey string -} - -type database struct { - databaseName string - provider string - host string - user string - password string - port int - ssl *bool - ca *string -} - -type auth struct { - url string - realm string - ssl string - realmKey string - controllerClient string - controllerSecret string - viewerClient string -} - -type events struct { - auditEnabled *bool // nil if not configured, pointer to bool if configured - retentionDays int - cleanupInterval int - captureIpAddress *bool // nil if not configured, pointer to bool if configured -} - -type ControllerProcedures struct { - check Entrypoint `yaml:"-"` // Check prereqs script (runs for default and custom procedures) - Deps Entrypoint `yaml:"deps,omitempty"` - SetEnv Entrypoint `yaml:"setEnv,omitempty"` - Install Entrypoint `yaml:"install,omitempty"` - Uninstall Entrypoint `yaml:"uninstall,omitempty"` - scriptNames []string `yaml:"-"` // List of all script names to be pushed to Controller - scriptContents []string `yaml:"-"` // List of contents of scripts to be pushed to Controller -} - -type Controller struct { - *ControllerOptions - ssh *util.SecureShellClient - db database - auth auth - events events - ctrlDir string - iofogDir string - procs ControllerProcedures - customInstall bool // Flag set when custom install scripts are provided - // svcDir string -} - -func NewController(options *ControllerOptions) (*Controller, error) { - ssh, err := util.NewSecureShellClient(options.User, options.Host, options.PrivKeyFilename) - if err != nil { - return nil, err - } - ssh.SetPort(options.Port) - if options.Image == "" { - options.Image = util.GetControllerImage() - } - ctrlDir := pkg.controllerDir - ctrl := &Controller{ - ControllerOptions: options, - ssh: ssh, - iofogDir: pkg.iofogDir, - ctrlDir: ctrlDir, - procs: ControllerProcedures{ - check: Entrypoint{ - Name: pkg.controllerScriptPrereq, - destPath: fmt.Sprintf("%s/%s", ctrlDir, pkg.controllerScriptPrereq), - }, - Deps: Entrypoint{ - Name: pkg.controllerScriptInstallContainerEngine, - destPath: fmt.Sprintf("%s/%s", ctrlDir, pkg.controllerScriptInstallContainerEngine), - }, - SetEnv: Entrypoint{ - Name: pkg.controllerScriptSetEnv, - destPath: fmt.Sprintf("%s/%s", ctrlDir, pkg.controllerScriptSetEnv), - }, - Install: Entrypoint{ - Name: pkg.controllerScriptInstall, - destPath: fmt.Sprintf("%s/%s", ctrlDir, pkg.controllerScriptInstall), - Args: []string{ - options.Image, - "", - "", - }, - }, - Uninstall: Entrypoint{ - Name: pkg.controllerScriptUninstall, - destPath: fmt.Sprintf("%s/%s", ctrlDir, pkg.controllerScriptUninstall), - }, - scriptNames: []string{ - pkg.controllerScriptPrereq, - pkg.controllerScriptInit, - pkg.controllerScriptInstallContainerEngine, - pkg.controllerScriptInstall, - pkg.controllerScriptSetEnv, - pkg.controllerScriptUninstall, - }, - }, - } - // Get script contents from embedded files - for _, scriptName := range ctrl.procs.scriptNames { - scriptContent, err := util.GetStaticFile(ctrl.addControllerAssetPrefix(scriptName)) - if err != nil { - return nil, err - } - ctrl.procs.scriptContents = append(ctrl.procs.scriptContents, scriptContent) - } - return ctrl, nil -} - -func (ctrl *Controller) SetControllerExternalDatabase(host, user, password, provider, databaseName string, port int, ssl *bool, ca *string) { - - ctrl.db = database{ - databaseName: databaseName, - provider: provider, - host: host, - user: user, - password: password, - port: port, - ssl: ssl, - ca: ca, - } -} - -func (ctrl *Controller) SetControllerAuth(url, realm, ssl, realmKey, controllerClient, controllerSecret, viewerClient string) { - - ctrl.auth = auth{ - url: url, - realm: realm, - ssl: ssl, - realmKey: realmKey, - controllerClient: controllerClient, - controllerSecret: controllerSecret, - viewerClient: viewerClient, - } -} - -func (ctrl *Controller) SetControllerEvents(auditEnabled bool, retentionDays, cleanupInterval int, captureIpAddress bool) { - ctrl.events = events{ - auditEnabled: &auditEnabled, - retentionDays: retentionDays, - cleanupInterval: cleanupInterval, - captureIpAddress: &captureIpAddress, - } -} - -func (ctrl *Controller) addControllerAssetPrefix(file string) string { - if ctrl.Airgap { - return fmt.Sprintf("%s/%s", controllerAssetPrefixAirgap, file) - } - return fmt.Sprintf("%s/%s", controllerAssetPrefixContainer, file) -} - -func (ctrl *Controller) CustomizeProcedures(dir string, procs *ControllerProcedures) error { - // Format source directory of script files - dir, err := util.FormatPath(dir) - if err != nil { - return err - } - - // Load script files into memory - files, err := os.ReadDir(dir) - if err != nil { - return err - } - for _, file := range files { - if !file.IsDir() { - procs.scriptNames = append(procs.scriptNames, file.Name()) - content, err := os.ReadFile(filepath.Join(dir, file.Name())) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, string(content)) - } - } - - // Add check_prereqs script and entrypoint (always required for both default and custom) - procs.scriptNames = append(procs.scriptNames, pkg.controllerScriptPrereq) - prereqContent, err := util.GetStaticFile(ctrl.addControllerAssetPrefix(pkg.controllerScriptPrereq)) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, prereqContent) - procs.check.destPath = fmt.Sprintf("%s/%s", ctrl.ctrlDir, pkg.controllerScriptPrereq) - - // Add default entrypoints and scripts if necessary (user not provided) - if procs.Deps.Name == "" { - procs.Deps = ctrl.procs.Deps - procs.scriptNames = append(procs.scriptNames, pkg.controllerScriptInstallContainerEngine) - scriptContent, err := util.GetStaticFile(ctrl.addControllerAssetPrefix(pkg.controllerScriptInstallContainerEngine)) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, scriptContent) - } - if procs.SetEnv.Name == "" { - procs.SetEnv = ctrl.procs.SetEnv - procs.scriptNames = append(procs.scriptNames, pkg.controllerScriptSetEnv) - scriptContent, err := util.GetStaticFile(ctrl.addControllerAssetPrefix(pkg.controllerScriptSetEnv)) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, scriptContent) - } - if procs.Install.Name == "" { - procs.Install = ctrl.procs.Install - procs.scriptNames = append(procs.scriptNames, pkg.controllerScriptInstall) - scriptContent, err := util.GetStaticFile(ctrl.addControllerAssetPrefix(pkg.controllerScriptInstall)) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, scriptContent) - } else { - ctrl.customInstall = true - } - if procs.Uninstall.Name == "" { - procs.Uninstall = ctrl.procs.Uninstall - procs.scriptNames = append(procs.scriptNames, pkg.controllerScriptUninstall) - scriptContent, err := util.GetStaticFile(ctrl.addControllerAssetPrefix(pkg.controllerScriptUninstall)) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, scriptContent) - } - - // Set destination paths where scripts appear on Controller - procs.Deps.destPath = fmt.Sprintf("%s/%s", ctrl.ctrlDir, procs.Deps.Name) - procs.SetEnv.destPath = fmt.Sprintf("%s/%s", ctrl.ctrlDir, procs.SetEnv.Name) - procs.Install.destPath = fmt.Sprintf("%s/%s", ctrl.ctrlDir, procs.Install.Name) - procs.Uninstall.destPath = fmt.Sprintf("%s/%s", ctrl.ctrlDir, procs.Uninstall.Name) - - ctrl.procs = *procs - return nil -} - -func (ctrl *Controller) copyScriptsToController() error { - // Ensure SSH connection is established (no-op if already connected) - if err := ctrl.ssh.Connect(); err != nil { - return err - } - - // Copy scripts to remote host - for idx, script := range ctrl.procs.scriptNames { - content := ctrl.procs.scriptContents[idx] - reader := strings.NewReader(content) - if err := ctrl.ssh.CopyTo(reader, ctrl.ctrlDir, script, "0775", int64(len(content))); err != nil { - return err - } - } - return nil -} - -func (ctrl *Controller) CopyScript(srcDir, filename, destDir string) (err error) { - // Read script from assets - if srcDir != "" { - srcDir = util.AddTrailingSlash(srcDir) - } - staticFile, err := util.GetStaticFile(srcDir + filename) - if err != nil { - return err - } - - // Copy to /tmp for backwards compatability - reader := strings.NewReader(staticFile) - if err := ctrl.ssh.CopyTo(reader, destDir, filename, "0775", int64(len(staticFile))); err != nil { - return err - } - - return nil +// GlobalCertificates holds optional router/NATS CA blocks deployed once via Controller API. +type GlobalCertificates struct { + RouterSiteCA *SiteCertificate + RouterLocalCA *SiteCertificate + NatsSiteCA *SiteCertificate + NatsLocalCA *SiteCertificate } -func (ctrl *Controller) Uninstall() (err error) { - // Stop controller gracefully - if err = ctrl.Stop(); err != nil { - return - } - - // Connect to server - Verbose("Connecting to server") - if err = ctrl.ssh.Connect(); err != nil { - return - } - defer util.Log(ctrl.ssh.Disconnect) - - // Copy uninstallation scripts to remote host - Verbose("Copying uninstall files to server") - if _, err = ctrl.ssh.Run(fmt.Sprintf("sudo mkdir -p %s && sudo chmod -R 0777 %s/", ctrl.ctrlDir, ctrl.ctrlDir)); err != nil { - return err - } - - // Use custom scripts if available, otherwise use default embedded scripts - if ctrl.customInstall || len(ctrl.procs.scriptNames) > 0 { - // Copy uninstall script specifically - uninstallIdx := -1 - for idx, scriptName := range ctrl.procs.scriptNames { - if scriptName == pkg.controllerScriptUninstall { - uninstallIdx = idx - break - } - } - if uninstallIdx >= 0 { - content := ctrl.procs.scriptContents[uninstallIdx] - reader := strings.NewReader(content) - if err := ctrl.ssh.CopyTo(reader, ctrl.ctrlDir, pkg.controllerScriptUninstall, "0775", int64(len(content))); err != nil { - return err - } - } else { - // Fallback to default uninstall script - assetPrefix := controllerAssetPrefixContainer - if ctrl.Airgap { - assetPrefix = controllerAssetPrefixAirgap - } - if err := ctrl.CopyScript(assetPrefix, pkg.controllerScriptUninstall, ctrl.ctrlDir); err != nil { - return err - } - } - } else { - // Fallback to default method for backward compatibility - assetPrefix := controllerAssetPrefixContainer - if ctrl.Airgap { - assetPrefix = controllerAssetPrefixAirgap - } - if err := ctrl.CopyScript(assetPrefix, pkg.controllerScriptUninstall, ctrl.ctrlDir); err != nil { - return err - } - } - - // Use uninstall entrypoint if available - uninstallCmd := ctrl.procs.Uninstall.getCommand() - if uninstallCmd == "" { - uninstallCmd = fmt.Sprintf("%s/%s", ctrl.ctrlDir, pkg.controllerScriptUninstall) - } - - cmds := []command{ - { - cmd: fmt.Sprintf("sudo %s", uninstallCmd), - msg: "Uninstalling controller on host " + ctrl.Host, - }, - } - - // Execute commands - for _, cmd := range cmds { - Verbose(cmd.msg) - _, err = ctrl.ssh.Run(cmd.cmd) - if err != nil { - return - } - } - return nil +func (g GlobalCertificates) empty() bool { + return g.RouterSiteCA == nil && g.RouterLocalCA == nil && g.NatsSiteCA == nil && g.NatsLocalCA == nil } -func (ctrl *Controller) copyInstallScripts() error { - Verbose("Copying install files to server") - if _, err := ctrl.ssh.Run(fmt.Sprintf("sudo mkdir -p %s && sudo chmod -R 0777 %s/", ctrl.ctrlDir, ctrl.ctrlDir)); err != nil { - return err - } - - // Use custom scripts if available, otherwise use default embedded scripts - if ctrl.customInstall || len(ctrl.procs.scriptNames) > 0 { - return ctrl.copyScriptsToController() +// DeployGlobalCertificates uploads router and NATS site/local CA secrets once (authenticated client). +func DeployGlobalCertificates(clt *client.Client, certs GlobalCertificates) error { + if clt == nil || certs.empty() { + return nil } - - // Fallback to default method for backward compatibility - assetPrefix := controllerAssetPrefixContainer - if ctrl.Airgap { - assetPrefix = controllerAssetPrefixAirgap - } - scripts := []string{ - pkg.controllerScriptPrereq, - pkg.controllerScriptInit, - pkg.controllerScriptInstallContainerEngine, - pkg.controllerScriptInstall, - pkg.controllerScriptSetEnv, + pairs := []struct { + name string + cert *SiteCertificate + }{ + {"router-site-ca", certs.RouterSiteCA}, + {"default-router-local-ca", certs.RouterLocalCA}, + {"nats-site-ca", certs.NatsSiteCA}, + {"default-nats-local-ca", certs.NatsLocalCA}, } - for _, script := range scripts { - if err := ctrl.CopyScript(assetPrefix, script, ctrl.ctrlDir); err != nil { + for _, pair := range pairs { + if err := deployGlobalCertificate(clt, pair.name, pair.cert); err != nil { return err } } return nil } -func appendControllerBaseEnv(env []string, ctrl *Controller) []string { - env = append(env, "CONTROL_PLANE=Remote") - env = append(env, fmt.Sprintf("\"CONTROLLER_NAMESPACE=%s\"", ctrl.Namespace)) - if ctrl.Https != nil && ctrl.Https.Enabled != nil && *ctrl.Https.Enabled { - env = append(env, fmt.Sprintf("\"SERVER_DEV_MODE=%s\"", "false")) - env = append(env, fmt.Sprintf("\"SSL_BASE64_CERT=%s\"", ctrl.Https.TLSCert)) - env = append(env, fmt.Sprintf("\"SSL_BASE64_KEY=%s\"", ctrl.Https.TLSKey)) - } - if ctrl.Https != nil && ctrl.Https.CACert != "" { - env = append(env, fmt.Sprintf("\"SSL_BASE64_INTERMEDIATE_CERT=%s\"", ctrl.Https.CACert)) - } - if ctrl.Host != "" { - env = append(env, fmt.Sprintf(`"CONTROLLER_HOST=%s"`, ctrl.Host)) - } - if ctrl.PidBaseDir != "" { - env = append(env, fmt.Sprintf("\"PID_BASE=%s\"", ctrl.PidBaseDir)) - } - if ctrl.EcnViewerPort != 0 { - env = append(env, fmt.Sprintf("\"VIEWER_PORT=%d\"", ctrl.EcnViewerPort)) - } - if ctrl.EcnViewerURL != "" { - env = append(env, fmt.Sprintf("\"VIEWER_URL=%s\"", ctrl.EcnViewerURL)) - } - if ctrl.SystemMicroservices.Router.X86 != "" { - env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_1=%s\"", ctrl.SystemMicroservices.Router.X86)) - } - if ctrl.SystemMicroservices.Router.ARM != "" { - env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_2=%s\"", ctrl.SystemMicroservices.Router.ARM)) - } - return env -} - -func appendDBEnv(env []string, ctrl *Controller) []string { - if ctrl.db.host != "" { - env = append(env, - fmt.Sprintf(`"DB_PROVIDER=%s"`, ctrl.db.provider), - fmt.Sprintf(`"DB_HOST=%s"`, ctrl.db.host), - fmt.Sprintf(`"DB_USERNAME=%s"`, ctrl.db.user), - fmt.Sprintf(`"DB_PASSWORD=%s"`, ctrl.db.password), - fmt.Sprintf(`"DB_PORT=%d"`, ctrl.db.port), - fmt.Sprintf(`"DB_NAME=%s"`, ctrl.db.databaseName)) - } - if ctrl.db.ssl != nil { - env = append(env, fmt.Sprintf(`"DB_USE_SSL=%t"`, *ctrl.db.ssl)) - } - if ctrl.db.ca != nil { - env = append(env, fmt.Sprintf(`"DB_SSL_CA=%s"`, *ctrl.db.ca)) - } - return env +type globalCertDeployer interface { + GetSecret(name string) (*client.SecretInfo, error) + GetCA(name string) (*client.CAInfo, error) + CreateSecret(request *client.SecretCreateRequest) error + CreateCA(request *client.CACreateRequest) error } -func appendAuthEnv(env []string, ctrl *Controller) []string { - if ctrl.auth.url != "" { - env = append(env, - fmt.Sprintf(`"KC_URL=%s"`, ctrl.auth.url), - fmt.Sprintf(`"KC_REALM=%s"`, ctrl.auth.realm), - fmt.Sprintf(`"KC_SSL_REQ=%s"`, ctrl.auth.ssl), - fmt.Sprintf(`"KC_REALM_KEY=%s"`, ctrl.auth.realmKey), - fmt.Sprintf(`"KC_CLIENT=%s"`, ctrl.auth.controllerClient), - fmt.Sprintf(`"KC_CLIENT_SECRET=%s"`, ctrl.auth.controllerSecret), - fmt.Sprintf(`"KC_VIEWER_CLIENT=%s"`, ctrl.auth.viewerClient)) - } - return env +func isControllerNotFound(err error) bool { + var notFound *client.NotFoundError + return errors.As(err, ¬Found) } -func appendNatsEnv(env []string, ctrl *Controller) []string { - natsX86 := ctrl.SystemMicroservices.Nats.X86 - natsARM := ctrl.SystemMicroservices.Nats.ARM - if natsX86 == "" && natsARM != "" { - natsX86 = natsARM - } - if natsARM == "" && natsX86 != "" { - natsARM = natsX86 - } - if natsX86 == "" && natsARM == "" { - natsX86 = util.GetNatsImage() - natsARM = natsX86 - } - if natsX86 != "" { - env = append(env, fmt.Sprintf("\"NATS_IMAGE_1=%s\"", natsX86)) - } - if natsARM != "" { - env = append(env, fmt.Sprintf("\"NATS_IMAGE_2=%s\"", natsARM)) - } - natsEnabled := true - if ctrl.NatsEnabled != nil { - natsEnabled = *ctrl.NatsEnabled - } - env = append(env, fmt.Sprintf("\"NATS_ENABLED=%t\"", natsEnabled)) - return env +func isControllerConflict(err error) bool { + var conflict *client.ConflictError + return errors.As(err, &conflict) } -func appendVaultEnv(env []string, ctrl *Controller) []string { - if ctrl.Vault == nil { - return env - } - if ctrl.Vault.Enabled != nil { - env = append(env, fmt.Sprintf("\"VAULT_ENABLED=%t\"", *ctrl.Vault.Enabled)) +func controllerSecretExists(clt globalCertDeployer, name string) (bool, error) { + _, err := clt.GetSecret(name) + if err == nil { + return true, nil } - if ctrl.Vault.Provider != "" { - env = append(env, fmt.Sprintf("\"VAULT_PROVIDER=%s\"", ctrl.Vault.Provider)) + if isControllerNotFound(err) { + return false, nil } - if ctrl.Vault.BasePath != "" { - env = append(env, fmt.Sprintf("\"VAULT_BASE_PATH=%s\"", ctrl.Vault.BasePath)) - } - env = appendVaultHashicorpEnv(env, ctrl.Vault.Hashicorp) - env = appendVaultAwsEnv(env, ctrl.Vault.Aws) - env = appendVaultAzureEnv(env, ctrl.Vault.Azure) - env = appendVaultGoogleEnv(env, ctrl.Vault.Google) - return env + return false, err } -func appendVaultHashicorpEnv(env []string, h *VaultHashicorpConfig) []string { - if h == nil { - return env - } - if h.Address != "" { - env = append(env, fmt.Sprintf("\"VAULT_HASHICORP_ADDRESS=%s\"", h.Address)) +func controllerCAExists(clt globalCertDeployer, name string) (bool, error) { + _, err := clt.GetCA(name) + if err == nil { + return true, nil } - if h.Token != "" { - env = append(env, fmt.Sprintf("\"VAULT_HASHICORP_TOKEN=%s\"", h.Token)) + if isControllerNotFound(err) { + return false, nil } - if h.Mount != "" { - env = append(env, fmt.Sprintf("\"VAULT_HASHICORP_MOUNT=%s\"", h.Mount)) - } - return env -} - -func appendVaultAwsEnv(env []string, a *VaultAwsConfig) []string { - if a == nil { - return env - } - if a.Region != "" { - env = append(env, fmt.Sprintf("\"VAULT_AWS_REGION=%s\"", a.Region)) - } - if a.AccessKeyId != "" { - env = append(env, fmt.Sprintf("\"VAULT_AWS_ACCESS_KEY_ID=%s\"", a.AccessKeyId)) - } - if a.AccessKey != "" { - env = append(env, fmt.Sprintf("\"VAULT_AWS_ACCESS_KEY=%s\"", a.AccessKey)) - } - return env + return false, err } -func appendVaultAzureEnv(env []string, a *VaultAzureConfig) []string { - if a == nil { - return env - } - if a.URL != "" { - env = append(env, fmt.Sprintf("\"VAULT_AZURE_URL=%s\"", a.URL)) +func deployGlobalCertificate(clt globalCertDeployer, secretName string, cert *SiteCertificate) error { + if cert == nil { + return nil } - if a.TenantId != "" { - env = append(env, fmt.Sprintf("\"VAULT_AZURE_TENANT_ID=%s\"", a.TenantId)) - } - if a.ClientId != "" { - env = append(env, fmt.Sprintf("\"VAULT_AZURE_CLIENT_ID=%s\"", a.ClientId)) - } - if a.ClientSecret != "" { - env = append(env, fmt.Sprintf("\"VAULT_AZURE_CLIENT_SECRET=%s\"", a.ClientSecret)) - } - return env -} -func appendVaultGoogleEnv(env []string, g *VaultGoogleConfig) []string { - if g == nil { - return env - } - if g.ProjectId != "" { - env = append(env, fmt.Sprintf("\"VAULT_GOOGLE_PROJECT_ID=%s\"", g.ProjectId)) + secretExists, err := controllerSecretExists(clt, secretName) + if err != nil { + return err } - if g.Credentials != "" { - env = append(env, fmt.Sprintf("\"VAULT_GOOGLE_CREDENTIALS=%s\"", g.Credentials)) + caExists, err := controllerCAExists(clt, secretName) + if err != nil { + return err } - return env -} - -func appendLogLevelEnv(env []string, ctrl *Controller) []string { - if ctrl.LogLevel != "" { - env = append(env, fmt.Sprintf("\"LOG_LEVEL=%s\"", ctrl.LogLevel)) + if secretExists && caExists { + return nil } - return env -} -func appendEventsEnv(env []string, ctrl *Controller) []string { - if ctrl.events.auditEnabled == nil { - return env - } - env = append(env, fmt.Sprintf("\"EVENT_AUDIT_ENABLED=%t\"", *ctrl.events.auditEnabled)) - if *ctrl.events.auditEnabled { - if ctrl.events.retentionDays != 0 { - env = append(env, fmt.Sprintf("\"EVENT_RETENTION_DAYS=%d\"", ctrl.events.retentionDays)) - } - if ctrl.events.cleanupInterval != 0 { - env = append(env, fmt.Sprintf("\"EVENT_CLEANUP_INTERVAL=%d\"", ctrl.events.cleanupInterval)) + if !secretExists { + secretRequest := client.SecretCreateRequest{ + Name: secretName, + Type: "tls", + Data: map[string]string{ + "ca.crt": cert.TLSCert, + "tls.crt": cert.TLSCert, + "tls.key": cert.TLSKey, + }, } - } - if ctrl.events.captureIpAddress != nil { - env = append(env, fmt.Sprintf("\"EVENT_CAPTURE_IP_ADDRESS=%t\"", *ctrl.events.captureIpAddress)) - } - return env -} - -func (ctrl *Controller) prepareEnvironmentVariables() string { - env := []string{} - env = appendControllerBaseEnv(env, ctrl) - env = appendDBEnv(env, ctrl) - env = appendAuthEnv(env, ctrl) - env = appendNatsEnv(env, ctrl) - env = appendVaultEnv(env, ctrl) - env = appendLogLevelEnv(env, ctrl) - env = appendEventsEnv(env, ctrl) - return strings.Join(env, " ") -} - -func (ctrl *Controller) prepareCommands(envString string) []command { - // Define commands - use custom entrypoints if available - checkPrereqsCmd := ctrl.procs.check.getCommand() - if checkPrereqsCmd == "" { - checkPrereqsCmd = fmt.Sprintf("%s/%s", ctrl.ctrlDir, pkg.controllerScriptPrereq) - } - depsCmd := ctrl.procs.Deps.getCommand() - if depsCmd == "" { - depsCmd = fmt.Sprintf("sudo %s/%s", ctrl.ctrlDir, pkg.controllerScriptInstallContainerEngine) - } else { - depsCmd = fmt.Sprintf("sudo %s", depsCmd) - } - setEnvCmd := ctrl.procs.SetEnv.getCommand() - if setEnvCmd == "" { - setEnvCmd = fmt.Sprintf("sudo %s/%s %s", ctrl.ctrlDir, pkg.controllerScriptSetEnv, envString) - } else { - setEnvCmd = fmt.Sprintf("sudo %s %s", setEnvCmd, envString) - } - installCmd := ctrl.procs.Install.getCommand() - if installCmd == "" { - installCmd = fmt.Sprintf("sudo %s/%s %s", ctrl.ctrlDir, pkg.controllerScriptInstall, ctrl.Image) - } else { - installCmd = fmt.Sprintf("sudo %s", installCmd) - } - - return []command{ - { - cmd: checkPrereqsCmd, - msg: "Checking prerequisites on Controller " + ctrl.Host, - }, - { - cmd: depsCmd, - msg: "Installing dependencies on Controller " + ctrl.Host, - }, - { - cmd: setEnvCmd, - msg: "Setting up environment variables for Controller " + ctrl.Host, - }, - { - cmd: installCmd, - msg: "Installing ioFog on Controller " + ctrl.Host, - }, - } -} - -func (ctrl *Controller) executeCommands(cmds []command) error { - for _, cmd := range cmds { - Verbose(cmd.msg) - _, err := ctrl.ssh.Run(cmd.cmd) - if err != nil { + if err := clt.CreateSecret(&secretRequest); err != nil && !isControllerConflict(err) { return err } } - return nil -} - -func (ctrl *Controller) waitForControllerToStart() (string, error) { - // Specify errors to ignore while waiting - ignoredErrors := []string{ - "Process exited with status 7", // curl: (7) Failed to connect to localhost port 8080: Connection refused - } - - // Add a small delay before checking - time.Sleep(5 * time.Second) - - Verbose("Waiting for Controller " + ctrl.Host) - // Increase timeout or retry attempts - maxRetries := 15 - var protocol string - if ctrl.Https != nil && ctrl.Https.Enabled != nil && *ctrl.Https.Enabled { - protocol = "https" - } else { - protocol = "http" - } - for i := 0; i < maxRetries; i++ { - Verbose(fmt.Sprintf("Try %d of %d", i+1, maxRetries)) - err := ctrl.ssh.RunUntil( - regexp.MustCompile("\"status\":\"online\""), - fmt.Sprintf("curl --request GET --url %s://localhost:%s/api/v3/status", protocol, iofog.ControllerPortString), - ignoredErrors, - ) - if err == nil { - return protocol, nil - } - time.Sleep(2 * time.Second * time.Duration(i+1)) // Exponential backoff - } - return "", fmt.Errorf("controller failed to start after %d retries", maxRetries) -} -func (ctrl *Controller) deployRouterCertificates(endpoint string) error { - if ctrl.SiteCA != nil { - if err := DeployRouterSecrets(endpoint, "router-site-ca", ctrl.SiteCA.TLSCert, ctrl.SiteCA.TLSKey); err != nil { - return err - } - if err := ImportRouterCertificate(endpoint, "router-site-ca"); err != nil { - return err + if !caExists { + caRequest := client.CACreateRequest{ + Name: secretName, + Type: "direct", + SecretName: secretName, } - } - // TODO: Remove LocalCA as it is only valid for k8s deployments. Also remove LocalCA from Remote ControllerOptions. - if ctrl.LocalCA != nil { - if err := DeployRouterSecrets(endpoint, "default-router-local-ca", ctrl.LocalCA.TLSCert, ctrl.LocalCA.TLSKey); err != nil { + if err := clt.CreateCA(&caRequest); err != nil && !isControllerConflict(err) { return err } - if err := ImportRouterCertificate(endpoint, "default-router-local-ca"); err != nil { - return err - } - } - return nil -} - -func (ctrl *Controller) Install() (err error) { - // Connect to server - Verbose("Connecting to server") - if err = ctrl.ssh.Connect(); err != nil { - return - } - defer util.Log(ctrl.ssh.Disconnect) - - // Copy installation scripts to remote host - if err = ctrl.copyInstallScripts(); err != nil { - return err - } - - // Prepare and build environment variables - envString := ctrl.prepareEnvironmentVariables() - - // Prepare commands - cmds := ctrl.prepareCommands(envString) - - // Execute commands - if err = ctrl.executeCommands(cmds); err != nil { - return err - } - - // Wait for controller to start - protocol, err := ctrl.waitForControllerToStart() - if err != nil { - return err - } - - // Wait for API - endpoint := fmt.Sprintf("%s://%s:%s", protocol, ctrl.Host, iofog.ControllerPortString) - if err = WaitForControllerAPI(endpoint); err != nil { - return - } - - // Deploy router certificates - if err = ctrl.deployRouterCertificates(endpoint); err != nil { - return - } - - return nil -} - -func (ctrl *Controller) Stop() (err error) { - // Connect to server - if err = ctrl.ssh.Connect(); err != nil { - return - } - defer util.Log(ctrl.ssh.Disconnect) - - // TODO: Clear the database - // Define commands - cmds := []string{ - "sudo service iofog-controller stop", - } - - // Execute commands - for _, cmd := range cmds { - _, err = ctrl.ssh.Run(cmd) - if err != nil { - return - } } - - return -} - -func WaitForControllerAPI(endpoint string) (err error) { - baseURL, err := util.GetBaseURL(endpoint) - if err != nil { - return err - } - ctrlClient := client.New(client.Options{BaseURL: baseURL}) - - seconds := 0 - for seconds < 60 { - // Try to create the user, return if success - if _, err = ctrlClient.GetStatus(); err == nil { - return - } - // Connection failed, wait and retry - time.Sleep(time.Millisecond * 1000) - seconds++ - } - - // Return last error - return -} - -func DeployRouterSecrets(endpoint, secretName string, TLSCert, TLSKey string) (err error) { - baseURL, err := util.GetBaseURL(endpoint) - if err != nil { - return err - } - ctrlClient := client.New(client.Options{BaseURL: baseURL}) - - request := client.SecretCreateRequest{ - Name: secretName, - Type: "tls", - Data: map[string]string{ - "TLSCert": TLSCert, - "TLSKey": TLSKey, - }, - } - - if err = ctrlClient.CreateSecret(&request); err != nil { - return err - } - return nil -} - -func ImportRouterCertificate(endpoint string, secretName string) (err error) { - baseURL, err := util.GetBaseURL(endpoint) - if err != nil { - return err - } - ctrlClient := client.New(client.Options{BaseURL: baseURL}) - - // Create CA certificate - request := client.CACreateRequest{ - Name: secretName, - Type: "direct", - SecretName: secretName, - } - - if err = ctrlClient.CreateCA(&request); err != nil { - return err - } - return nil } diff --git a/pkg/iofog/install/controller_certificates_test.go b/pkg/iofog/install/controller_certificates_test.go new file mode 100644 index 000000000..2e10e6dab --- /dev/null +++ b/pkg/iofog/install/controller_certificates_test.go @@ -0,0 +1,117 @@ +package install + +import ( + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/stretchr/testify/require" +) + +type fakeGlobalCertClient struct { + secrets map[string]struct{} + cas map[string]struct{} + createSecretCalls int + createCACalls int +} + +func (f *fakeGlobalCertClient) GetSecret(name string) (*client.SecretInfo, error) { + if _, ok := f.secrets[name]; ok { + return &client.SecretInfo{Name: name}, nil + } + return nil, client.NewNotFoundError(name) +} + +func (f *fakeGlobalCertClient) GetCA(name string) (*client.CAInfo, error) { + if _, ok := f.cas[name]; ok { + return &client.CAInfo{Name: name}, nil + } + return nil, client.NewNotFoundError(name) +} + +func (f *fakeGlobalCertClient) CreateSecret(request *client.SecretCreateRequest) error { + f.createSecretCalls++ + if f.secrets == nil { + f.secrets = map[string]struct{}{} + } + f.secrets[request.Name] = struct{}{} + return nil +} + +func (f *fakeGlobalCertClient) CreateCA(request *client.CACreateRequest) error { + f.createCACalls++ + if f.cas == nil { + f.cas = map[string]struct{}{} + } + f.cas[request.Name] = struct{}{} + return nil +} + +func testSiteCertificate() *SiteCertificate { + return &SiteCertificate{ + TLSCert: "cert-pem", + TLSKey: "key-pem", + } +} + +func TestDeployGlobalCertificateSkipsWhenBothExist(t *testing.T) { + clt := &fakeGlobalCertClient{ + secrets: map[string]struct{}{"router-site-ca": {}}, + cas: map[string]struct{}{"router-site-ca": {}}, + } + + require.NoError(t, deployGlobalCertificate(clt, "router-site-ca", testSiteCertificate())) + require.Equal(t, 0, clt.createSecretCalls) + require.Equal(t, 0, clt.createCACalls) +} + +func TestDeployGlobalCertificateCreatesBothWhenMissing(t *testing.T) { + clt := &fakeGlobalCertClient{} + + require.NoError(t, deployGlobalCertificate(clt, "router-site-ca", testSiteCertificate())) + require.Equal(t, 1, clt.createSecretCalls) + require.Equal(t, 1, clt.createCACalls) +} + +func TestDeployGlobalCertificateCreatesCAWhenSecretExists(t *testing.T) { + clt := &fakeGlobalCertClient{ + secrets: map[string]struct{}{"router-site-ca": {}}, + } + + require.NoError(t, deployGlobalCertificate(clt, "router-site-ca", testSiteCertificate())) + require.Equal(t, 0, clt.createSecretCalls) + require.Equal(t, 1, clt.createCACalls) +} + +func TestDeployGlobalCertificateCreatesSecretWhenCAExists(t *testing.T) { + clt := &fakeGlobalCertClient{ + cas: map[string]struct{}{"router-site-ca": {}}, + } + + require.NoError(t, deployGlobalCertificate(clt, "router-site-ca", testSiteCertificate())) + require.Equal(t, 1, clt.createSecretCalls) + require.Equal(t, 0, clt.createCACalls) +} + +func TestDeployGlobalCertificateNilCertNoOp(t *testing.T) { + clt := &fakeGlobalCertClient{} + require.NoError(t, deployGlobalCertificate(clt, "router-site-ca", nil)) + require.Equal(t, 0, clt.createSecretCalls) + require.Equal(t, 0, clt.createCACalls) +} + +func TestDeployGlobalCertificateTreatsCreateConflictAsExists(t *testing.T) { + clt := &conflictOnCreateGlobalCertClient{} + require.NoError(t, deployGlobalCertificate(clt, "router-site-ca", testSiteCertificate())) +} + +type conflictOnCreateGlobalCertClient struct { + fakeGlobalCertClient +} + +func (f *conflictOnCreateGlobalCertClient) CreateSecret(request *client.SecretCreateRequest) error { + return client.NewConflictError(request.Name) +} + +func (f *conflictOnCreateGlobalCertClient) CreateCA(request *client.CACreateRequest) error { + return client.NewConflictError(request.Name) +} diff --git a/pkg/iofog/install/edgelet_config.go b/pkg/iofog/install/edgelet_config.go new file mode 100644 index 000000000..7150c488c --- /dev/null +++ b/pkg/iofog/install/edgelet_config.go @@ -0,0 +1,611 @@ +package install + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" +) + +const ( + edgeletConfigAsset = "edgelet/edgelet-config.yaml" + edgeletSampleCAAsset = "edgelet/edgelet-controller-ca.crt" + edgeletDefaultProfile = "production" +) + +// EdgeletRuntimeSpec carries spec.config fields used to materialize edgelet profile YAML. +type EdgeletRuntimeSpec struct { + Arch string + Latitude float64 + Longitude float64 + Agent client.AgentConfiguration +} + +// EdgeletPlatformPaths holds on-host edgelet config and cert locations. +type EdgeletPlatformPaths struct { + ConfigDir string + ConfigFile string + CertFile string +} + +// EdgeletPaths returns config and cert paths for a host OS (linux, darwin, windows). +func EdgeletPaths(hostOS string) EdgeletPlatformPaths { + switch normalizeEdgeletHostOS(hostOS) { + case "windows": + base := `%ProgramData%\Edgelet\config` + return EdgeletPlatformPaths{ + ConfigDir: base, + ConfigFile: base + `\config.yaml`, + CertFile: base + `\cert.crt`, + } + default: + return EdgeletPlatformPaths{ + ConfigDir: "/etc/edgelet", + ConfigFile: "/etc/edgelet/config.yaml", + CertFile: "/etc/edgelet/cert.crt", + } + } +} + +func normalizeEdgeletHostOS(hostOS string) string { + switch strings.ToLower(strings.TrimSpace(hostOS)) { + case "darwin", "macos", "osx": + return "darwin" + case "windows", "windows_nt": + return "windows" + default: + return "linux" + } +} + +func loadEdgeletConfigTemplate() (map[string]interface{}, error) { + raw, err := util.GetStaticFile(edgeletConfigAsset) + if err != nil { + return nil, err + } + var doc map[string]interface{} + if err := yaml.Unmarshal([]byte(raw), &doc); err != nil { + return nil, fmt.Errorf("parse edgelet config template: %w", err) + } + return doc, nil +} + +func loadEdgeletSampleCA() ([]byte, error) { + raw, err := util.GetStaticFile(edgeletSampleCAAsset) + if err != nil { + return nil, err + } + return []byte(raw), nil +} + +func productionProfile(doc map[string]interface{}) (map[string]interface{}, error) { + profiles, ok := doc["profiles"].(map[interface{}]interface{}) + if !ok { + return nil, fmt.Errorf("edgelet config template missing profiles") + } + raw, ok := profiles[edgeletDefaultProfile] + if !ok { + return nil, fmt.Errorf("edgelet config template missing %q profile", edgeletDefaultProfile) + } + profile, ok := raw.(map[interface{}]interface{}) + if !ok { + return nil, fmt.Errorf("edgelet config template profile has unexpected shape") + } + out := make(map[string]interface{}, len(profile)) + for k, v := range profile { + key, ok := k.(string) + if !ok { + return nil, fmt.Errorf("edgelet config template profile key has unexpected type") + } + out[key] = v + } + return out, nil +} + +func defaultContainerEngineURL(engine string) string { + switch strings.ToLower(strings.TrimSpace(engine)) { + case "docker": + return "unix:///var/run/docker.sock" + case "podman": + return "unix:///run/podman/podman.sock" + default: + return "unix:///run/edgelet/containerd.sock" + } +} + +func resolveContainerEngine(cfg *client.AgentConfiguration) string { + if cfg != nil && cfg.ContainerEngine != nil && strings.TrimSpace(*cfg.ContainerEngine) != "" { + return strings.TrimSpace(*cfg.ContainerEngine) + } + return "edgelet" +} + +func resolveContainerEngineURL(engine string, cfg *client.AgentConfiguration) string { + if cfg != nil && cfg.ContainerEngineURL != nil && strings.TrimSpace(*cfg.ContainerEngineURL) != "" { + return strings.TrimSpace(*cfg.ContainerEngineURL) + } + return defaultContainerEngineURL(engine) +} + +func boolToOnOff(v bool) string { + if v { + return "on" + } + return "off" +} + +func formatFloat(v float64) string { + return strconv.FormatFloat(v, 'f', -1, 64) +} + +func formatInt(v int64) string { + return strconv.FormatInt(v, 10) +} + +func normalizeLogLevel(level string) string { + level = strings.TrimSpace(level) + if level == "" { + return "INFO" + } + return strings.ToUpper(level) +} + +func applyEdgeletProfileOverrides(profile map[string]interface{}, hostOS, arch string, latitude, longitude float64, cfg *client.AgentConfiguration) { + paths := EdgeletPaths(hostOS) + profile["controllerCert"] = paths.CertFile + + if arch != "" { + profile["arch"] = arch + } + + engine := resolveContainerEngine(cfg) + profile["containerEngine"] = engine + profile["containerEngineUrl"] = resolveContainerEngineURL(engine, cfg) + + if cfg == nil { + return + } + + if cfg.NetworkInterface != nil && *cfg.NetworkInterface != "" { + profile["networkInterface"] = *cfg.NetworkInterface + } + if cfg.DiskLimit != nil { + profile["diskConsumptionLimit"] = formatInt(*cfg.DiskLimit) + } + if cfg.DiskDirectory != nil && *cfg.DiskDirectory != "" { + profile["diskDirectory"] = *cfg.DiskDirectory + } + if cfg.MemoryLimit != nil { + profile["memoryConsumptionLimit"] = formatInt(*cfg.MemoryLimit) + } + if cfg.CPULimit != nil { + profile["processorConsumptionLimit"] = formatInt(*cfg.CPULimit) + } + if cfg.LogLimit != nil { + profile["logDiskConsumptionLimit"] = formatInt(*cfg.LogLimit) + } + if cfg.LogDirectory != nil && *cfg.LogDirectory != "" { + profile["logDiskDirectory"] = *cfg.LogDirectory + } + if cfg.LogFileCount != nil { + profile["logFileCount"] = formatInt(*cfg.LogFileCount) + } + if cfg.StatusFrequency != nil { + profile["statusFrequency"] = formatFloat(*cfg.StatusFrequency) + } + if cfg.ChangeFrequency != nil { + profile["changeFrequency"] = formatFloat(*cfg.ChangeFrequency) + } + if cfg.DeviceScanFrequency != nil { + profile["scanDevicesFreq"] = formatFloat(*cfg.DeviceScanFrequency) + } + if cfg.WatchdogEnabled != nil { + profile["watchdogEnabled"] = boolToOnOff(*cfg.WatchdogEnabled) + } + if cfg.GpsMode != nil && *cfg.GpsMode != "" { + profile["gps"] = *cfg.GpsMode + } + if latitude != 0 || longitude != 0 { + profile["gpsCoordinates"] = fmt.Sprintf("%g,%g", latitude, longitude) + } + if cfg.GpsDevice != nil { + profile["gpsDevice"] = *cfg.GpsDevice + } + if cfg.GpsScanFrequency != nil { + profile["gpsScanFrequency"] = formatFloat(*cfg.GpsScanFrequency) + } + if cfg.EdgeGuardFrequency != nil { + profile["edgeGuardFreq"] = formatFloat(*cfg.EdgeGuardFrequency) + } + if cfg.PruningFrequency != nil { + profile["pruningFrequency"] = formatFloat(*cfg.PruningFrequency) + } + if cfg.AvailableDiskThreshold != nil { + profile["availableDiskThreshold"] = formatFloat(*cfg.AvailableDiskThreshold) + } + if cfg.LogLevel != nil && *cfg.LogLevel != "" { + profile["logLevel"] = normalizeLogLevel(*cfg.LogLevel) + } + if cfg.TimeZone != "" { + profile["timeZone"] = cfg.TimeZone + } +} + +// BuildEdgeletConfigYAML renders edgelet runtime config from the embedded template and spec.config overrides. +func BuildEdgeletConfigYAML(hostOS, arch string, latitude, longitude float64, cfg *client.AgentConfiguration) ([]byte, error) { + doc, err := loadEdgeletConfigTemplate() + if err != nil { + return nil, err + } + profile, err := productionProfile(doc) + if err != nil { + return nil, err + } + applyEdgeletProfileOverrides(profile, hostOS, arch, latitude, longitude, cfg) + + profiles := map[interface{}]interface{}{ + edgeletDefaultProfile: profile, + } + doc["currentProfile"] = edgeletDefaultProfile + doc["profiles"] = profiles + + out, err := yaml.Marshal(doc) + if err != nil { + return nil, fmt.Errorf("marshal edgelet config: %w", err) + } + return out, nil +} + +// IsDesktopContainerHost reports macOS/Windows hosts that use desktop container runtimes. +func IsDesktopContainerHost(hostOS string) bool { + switch normalizeEdgeletHostOS(hostOS) { + case "darwin", "windows": + return true + default: + return false + } +} + +// IsDesktopContainerDeploy reports container edgelet on a desktop-class host OS. +func IsDesktopContainerDeploy(cfg EdgeletInstallConfig) bool { + return !cfg.native() && IsDesktopContainerHost(cfg.hostOS()) +} + +// ShouldMaterializeEdgeletRuntime reports whether host-side config files should be written before start. +func ShouldMaterializeEdgeletRuntime(cfg EdgeletInstallConfig) bool { + return !IsDesktopContainerDeploy(cfg) +} + +var edgeletBootstrapConfigSkipKeys = map[string]struct{}{ + "controllerUrl": {}, + "iofogUuid": {}, + "controllerCert": {}, +} + +// edgeletProfileKeyToConfigFlag maps edgelet-config.yaml profile keys to edgelet config CLI +// short aliases (see edgelet/docs/cli/generated/edgelet_config.md). +var edgeletProfileKeyToConfigFlag = map[string]string{ + "arch": "--ft", + "availableDiskThreshold": "--dt", + "changeFrequency": "--cf", + "containerEngine": "--ce", + "containerEngineUrl": "--cu", + "devMode": "--dev", + "diskDirectory": "--dl", + "diskConsumptionLimit": "--d", + "edgeGuardFreq": "--egf", + "gps": "--gps", + "gpsCoordinates": "--gpsc", + "gpsDevice": "--gpsd", + "gpsScanFrequency": "--gpsf", + "logDiskDirectory": "--ld", + "logDiskConsumptionLimit": "--l", + "logFileCount": "--lc", + "logLevel": "--ll", + "memoryConsumptionLimit": "--m", + "networkInterface": "--n", + "pruningFrequency": "--pf", + "processorConsumptionLimit": "--p", + "scanDevicesFreq": "--sd", + "secureMode": "--sec", + "statusFrequency": "--sf", + "timeZone": "--tz", + "upgradeScanFrequency": "--uf", + "watchdogEnabled": "--wd", +} + +// buildBootstrapProfileFromAgentSpec collects only spec.config fields explicitly set in +// the deploy agent YAML. Template defaults are not included. +func buildBootstrapProfileFromAgentSpec(arch string, latitude, longitude float64, cfg *client.AgentConfiguration) map[string]interface{} { + profile := make(map[string]interface{}) + + if arch != "" && arch != "auto" { + profile["arch"] = arch + } + if latitude != 0 || longitude != 0 { + profile["gpsCoordinates"] = fmt.Sprintf("%g,%g", latitude, longitude) + } + if cfg == nil { + return profile + } + + if cfg.ContainerEngine != nil { + engine := strings.TrimSpace(*cfg.ContainerEngine) + if engine != "" && !strings.EqualFold(engine, "edgelet") { + profile["containerEngine"] = engine + profile["containerEngineUrl"] = resolveContainerEngineURL(engine, cfg) + } + } + if cfg.ContainerEngineURL != nil { + if url := strings.TrimSpace(*cfg.ContainerEngineURL); url != "" { + profile["containerEngineUrl"] = url + } + } + if cfg.NetworkInterface != nil && *cfg.NetworkInterface != "" { + profile["networkInterface"] = *cfg.NetworkInterface + } + if cfg.DiskLimit != nil { + profile["diskConsumptionLimit"] = formatInt(*cfg.DiskLimit) + } + if cfg.DiskDirectory != nil && *cfg.DiskDirectory != "" { + profile["diskDirectory"] = *cfg.DiskDirectory + } + if cfg.MemoryLimit != nil { + profile["memoryConsumptionLimit"] = formatInt(*cfg.MemoryLimit) + } + if cfg.CPULimit != nil { + profile["processorConsumptionLimit"] = formatInt(*cfg.CPULimit) + } + if cfg.LogLimit != nil { + profile["logDiskConsumptionLimit"] = formatInt(*cfg.LogLimit) + } + if cfg.LogDirectory != nil && *cfg.LogDirectory != "" { + profile["logDiskDirectory"] = *cfg.LogDirectory + } + if cfg.LogFileCount != nil { + profile["logFileCount"] = formatInt(*cfg.LogFileCount) + } + if cfg.StatusFrequency != nil { + profile["statusFrequency"] = formatFloat(*cfg.StatusFrequency) + } + if cfg.ChangeFrequency != nil { + profile["changeFrequency"] = formatFloat(*cfg.ChangeFrequency) + } + if cfg.DeviceScanFrequency != nil { + profile["scanDevicesFreq"] = formatFloat(*cfg.DeviceScanFrequency) + } + if cfg.WatchdogEnabled != nil { + profile["watchdogEnabled"] = boolToOnOff(*cfg.WatchdogEnabled) + } + if cfg.GpsMode != nil && *cfg.GpsMode != "" { + profile["gps"] = *cfg.GpsMode + } + if cfg.GpsDevice != nil && *cfg.GpsDevice != "" { + profile["gpsDevice"] = *cfg.GpsDevice + } + if cfg.GpsScanFrequency != nil { + profile["gpsScanFrequency"] = formatFloat(*cfg.GpsScanFrequency) + } + if cfg.EdgeGuardFrequency != nil { + profile["edgeGuardFreq"] = formatFloat(*cfg.EdgeGuardFrequency) + } + if cfg.PruningFrequency != nil { + profile["pruningFrequency"] = formatFloat(*cfg.PruningFrequency) + } + if cfg.AvailableDiskThreshold != nil { + profile["availableDiskThreshold"] = formatFloat(*cfg.AvailableDiskThreshold) + } + if cfg.LogLevel != nil && *cfg.LogLevel != "" { + profile["logLevel"] = normalizeLogLevel(*cfg.LogLevel) + } + if cfg.TimeZone != "" { + profile["timeZone"] = cfg.TimeZone + } + return profile +} + +// BuildEdgeletBootstrapConfigCommand renders edgelet config CLI args from agent spec for desktop container bootstrap. +func BuildEdgeletBootstrapConfigCommand(arch string, latitude, longitude float64, agentCfg *client.AgentConfiguration) (string, error) { + profile := buildBootstrapProfileFromAgentSpec(arch, latitude, longitude, agentCfg) + if len(profile) == 0 { + return "", nil + } + + args := []string{"edgelet", "config"} + for key, raw := range profile { + if _, skip := edgeletBootstrapConfigSkipKeys[key]; skip { + continue + } + flag, ok := edgeletProfileKeyToConfigFlag[key] + if !ok { + continue + } + value := strings.TrimSpace(fmt.Sprintf("%v", raw)) + if value == "" { + continue + } + args = append(args, flag, value) + } + if len(args) == 2 { + return "", nil + } + return shellJoinArgs(args), nil +} + +func (cfg EdgeletInstallConfig) bootstrapConfigCommand() (string, error) { + arch := cfg.Arch + latitude := 0.0 + longitude := 0.0 + var agentCfg *client.AgentConfiguration + if cfg.Runtime != nil { + agentCfg = &cfg.Runtime.Agent + latitude = cfg.Runtime.Latitude + longitude = cfg.Runtime.Longitude + if cfg.Runtime.Arch != "" { + arch = cfg.Runtime.Arch + } + } + return BuildEdgeletBootstrapConfigCommand(arch, latitude, longitude, agentCfg) +} + +func runtimeConfigDir(paths EdgeletPlatformPaths) string { + if paths.ConfigDir != "" { + return paths.ConfigDir + } + return filepath.Dir(paths.ConfigFile) +} + +func ensureLocalRuntimeConfigDir(paths EdgeletPlatformPaths, useSudo bool) error { + dir := runtimeConfigDir(paths) + if !useSudo { + return os.MkdirAll(dir, util.DirPerm) + } + if _, err := util.Exec("", "sudo", "mkdir", "-p", dir); err != nil { + return err + } + _, err := util.Exec("", "sudo", "chmod", "755", dir) + return err +} + +func localRuntimeFileExists(path string, useSudo bool) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + if !useSudo { + return false, err + } + // install.sh may create /etc/edgelet mode 750; unprivileged stat returns EACCES. + if _, err := util.Exec("", "sudo", "test", "-f", path); err == nil { + return true, nil + } + return false, nil +} + +func writeLocalFileIfMissing(path string, content []byte, perm os.FileMode, useSudo bool) error { + exists, err := localRuntimeFileExists(path, useSudo) + if err != nil { + return err + } + if exists { + return nil + } + + if err := os.MkdirAll(filepath.Dir(path), util.DirPerm); err != nil && !useSudo { + return err + } + + if !useSudo { + return os.WriteFile(path, content, perm) + } + + tmp, err := os.CreateTemp("", "edgelet-runtime-*") + if err != nil { + return err + } + tmpPath := tmp.Name() + defer os.Remove(tmpPath) + + if _, err := tmp.Write(content); err != nil { + util.IgnoreClose(tmp) + return err + } + if err := tmp.Close(); err != nil { + return err + } + + if _, err := util.Exec("", "sudo", "mkdir", "-p", filepath.Dir(path)); err != nil { + return err + } + mode := fmt.Sprintf("%04o", perm) + if _, err := util.Exec("", "sudo", "install", "-m", mode, tmpPath, path); err != nil { + return err + } + return nil +} + +func materializeEdgeletRuntimeTo(paths EdgeletPlatformPaths, hostOS, arch string, spec *EdgeletRuntimeSpec, useSudo bool) error { + if err := ensureLocalRuntimeConfigDir(paths, useSudo); err != nil { + return fmt.Errorf("prepare edgelet config dir: %w", err) + } + + var agentCfg *client.AgentConfiguration + latitude := 0.0 + longitude := 0.0 + if spec != nil { + agentCfg = &spec.Agent + latitude = spec.Latitude + longitude = spec.Longitude + if spec.Arch != "" { + arch = spec.Arch + } + } + + configYAML, err := BuildEdgeletConfigYAML(hostOS, arch, latitude, longitude, agentCfg) + if err != nil { + return err + } + if err := writeLocalFileIfMissing(paths.ConfigFile, configYAML, 0o640, useSudo); err != nil { + return fmt.Errorf("write edgelet config: %w", err) + } + + sampleCA, err := loadEdgeletSampleCA() + if err != nil { + return err + } + if err := writeLocalFileIfMissing(paths.CertFile, sampleCA, 0o644, useSudo); err != nil { + return fmt.Errorf("write edgelet sample CA: %w", err) + } + return nil +} + +// MaterializeEdgeletRuntime writes edgelet config and sample CA on the local host when files are missing. +func MaterializeEdgeletRuntime(hostOS string, spec *EdgeletRuntimeSpec) error { + useSudo := normalizeEdgeletHostOS(hostOS) != "windows" + return materializeEdgeletRuntimeTo(EdgeletPaths(hostOS), hostOS, "", spec, useSudo) +} + +// EdgeletProvisionCommands builds edgelet config/provision command strings for a controller endpoint. +// +//nolint:revive // command is intentionally unexported; callers are in this package +func EdgeletProvisionCommands(controllerEndpoint, key, caCert string, useSudo bool) ([]command, error) { + if strings.TrimSpace(key) == "" { + return nil, fmt.Errorf("provisioning key is required") + } + + prefix := "" + if useSudo { + prefix = "sudo " + } + + controllerBaseURL, err := util.GetBaseURL(controllerEndpoint) + if err != nil { + return nil, err + } + + cmds := []command{ + { + cmd: prefix + "edgelet config --a " + controllerBaseURL.String(), + msg: "Configuring edgelet with Controller URL " + controllerBaseURL.String(), + }, + } + if strings.TrimSpace(caCert) != "" { + cmds = append(cmds, command{ + cmd: prefix + "edgelet config cert " + caCert, + msg: "Configuring edgelet with Controller CA certificate", + }) + } + cmds = append(cmds, command{ + cmd: prefix + "edgelet provision " + key, + msg: "Provisioning edgelet with Controller", + }) + return cmds, nil +} diff --git a/pkg/iofog/install/edgelet_config_test.go b/pkg/iofog/install/edgelet_config_test.go new file mode 100644 index 000000000..8133e1e8b --- /dev/null +++ b/pkg/iofog/install/edgelet_config_test.go @@ -0,0 +1,342 @@ +package install + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" +) + +func strPtr(v string) *string { return &v } +func int64Ptr(v int64) *int64 { return &v } +func float64Ptr(v float64) *float64 { return &v } +func boolPtr(v bool) *bool { return &v } + +func TestEdgeletPaths(t *testing.T) { + linux := EdgeletPaths("linux") + if linux.ConfigFile != "/etc/edgelet/config.yaml" || linux.CertFile != "/etc/edgelet/cert.crt" { + t.Fatalf("unexpected linux paths: %+v", linux) + } + + win := EdgeletPaths("windows") + if !strings.Contains(win.ConfigFile, `Edgelet\config\config.yaml`) { + t.Fatalf("unexpected windows config path: %q", win.ConfigFile) + } + if !strings.Contains(win.CertFile, `Edgelet\config\cert.crt`) { + t.Fatalf("unexpected windows cert path: %q", win.CertFile) + } +} + +func TestResolveContainerEngineURLDefaults(t *testing.T) { + tests := []struct { + engine string + want string + }{ + {"edgelet", "unix:///run/edgelet/containerd.sock"}, + {"docker", "unix:///var/run/docker.sock"}, + {"podman", "unix:///run/podman/podman.sock"}, + } + for _, tt := range tests { + cfg := &client.AgentConfiguration{ContainerEngine: strPtr(tt.engine)} + got := ResolveContainerEngineURL(tt.engine, cfg) + if got != tt.want { + t.Fatalf("engine=%q got %q want %q", tt.engine, got, tt.want) + } + } +} + +func TestBuildEdgeletConfigYAMLAppliesSpecConfig(t *testing.T) { + cfg := &client.AgentConfiguration{ + ContainerEngine: strPtr("docker"), + DiskLimit: int64Ptr(50), + MemoryLimit: int64Ptr(4096), + CPULimit: int64Ptr(80), + StatusFrequency: float64Ptr(10), + ChangeFrequency: float64Ptr(10), + WatchdogEnabled: boolPtr(false), + LogLevel: strPtr("info"), + AvailableDiskThreshold: float64Ptr(90), + TimeZone: "UTC", + } + + out, err := BuildEdgeletConfigYAML("linux", "amd64", 46.2, 6.14, cfg) + if err != nil { + t.Fatalf("BuildEdgeletConfigYAML: %v", err) + } + text := string(out) + for _, want := range []string{ + "currentProfile: production", + "containerEngine: docker", + "arch: amd64", + "diskConsumptionLimit: \"50\"", + "memoryConsumptionLimit: \"4096\"", + "processorConsumptionLimit: \"80\"", + "changeFrequency: \"10\"", + "statusFrequency: \"10\"", + "logLevel: INFO", + "controllerCert: /etc/edgelet/cert.crt", + "gpsCoordinates: 46.2,6.14", + } { + if !strings.Contains(text, want) { + t.Fatalf("config yaml missing %q:\n%s", want, text) + } + } +} + +func TestEdgeletProvisionCommands(t *testing.T) { + withCert, err := EdgeletProvisionCommands("https://controller.example.com", "provision-key", "base64-ca", true) + if err != nil { + t.Fatalf("EdgeletProvisionCommands: %v", err) + } + if len(withCert) != 3 { + t.Fatalf("expected 3 commands, got %d: %+v", len(withCert), withCert) + } + if withCert[0].cmd != "sudo edgelet config --a https://controller.example.com/api/v3" { + t.Fatalf("unexpected config URL command: %q", withCert[0].cmd) + } + if withCert[1].cmd != "sudo edgelet config cert base64-ca" { + t.Fatalf("unexpected cert command: %q", withCert[1].cmd) + } + if withCert[2].cmd != "sudo edgelet provision provision-key" { + t.Fatalf("unexpected provision command: %q", withCert[2].cmd) + } + + withoutCert, err := EdgeletProvisionCommands("http://localhost:51121", "key-only", "", false) + if err != nil { + t.Fatalf("EdgeletProvisionCommands without cert: %v", err) + } + if len(withoutCert) != 2 { + t.Fatalf("expected 2 commands without cert, got %d: %+v", len(withoutCert), withoutCert) + } + if strings.Contains(withoutCert[0].cmd, "sudo") { + t.Fatalf("expected no sudo prefix for local commands: %q", withoutCert[0].cmd) + } + if withoutCert[0].cmd != "edgelet config --a http://localhost:51121/api/v3" { + t.Fatalf("unexpected local config command: %q", withoutCert[0].cmd) + } +} + +func TestLocalRuntimeFileExists(t *testing.T) { + dir := t.TempDir() + missing := filepath.Join(dir, "config.yaml") + exists, err := localRuntimeFileExists(missing, false) + if err != nil { + t.Fatalf("localRuntimeFileExists missing: %v", err) + } + if exists { + t.Fatal("expected missing file") + } + + if err := os.WriteFile(missing, []byte("cfg"), 0o640); err != nil { + t.Fatalf("write file: %v", err) + } + exists, err = localRuntimeFileExists(missing, false) + if err != nil { + t.Fatalf("localRuntimeFileExists present: %v", err) + } + if !exists { + t.Fatal("expected existing file") + } +} + +func TestLocalRuntimeFileExistsStatPermissionDenied(t *testing.T) { + dir := t.TempDir() + restricted := filepath.Join(dir, "restricted") + if err := os.Mkdir(restricted, 0o000); err != nil { + t.Fatalf("mkdir restricted: %v", err) + } + t.Cleanup(func() { _ = os.Chmod(restricted, 0o700) }) + + target := filepath.Join(restricted, "config.yaml") + _, err := localRuntimeFileExists(target, false) + if err == nil { + t.Fatal("expected permission error without sudo") + } +} + +func TestMaterializeEdgeletRuntimeTo(t *testing.T) { + dir := t.TempDir() + paths := EdgeletPlatformPaths{ + ConfigDir: dir, + ConfigFile: filepath.Join(dir, "config.yaml"), + CertFile: filepath.Join(dir, "cert.crt"), + } + spec := &EdgeletRuntimeSpec{ + Arch: "arm64", + Agent: client.AgentConfiguration{ + ContainerEngine: strPtr("edgelet"), + }, + } + if err := materializeEdgeletRuntimeTo(paths, "linux", "", spec, false); err != nil { + t.Fatalf("materializeEdgeletRuntimeTo: %v", err) + } + configData, err := os.ReadFile(paths.ConfigFile) + if err != nil { + t.Fatalf("read config: %v", err) + } + if !strings.Contains(string(configData), "arch: arm64") { + t.Fatalf("config not written as expected: %s", configData) + } + certData, err := os.ReadFile(paths.CertFile) + if err != nil { + t.Fatalf("read cert: %v", err) + } + if !strings.Contains(string(certData), "BEGIN CERTIFICATE") { + t.Fatalf("sample CA not written") + } + + before := len(certData) + if err := materializeEdgeletRuntimeTo(paths, "linux", "", spec, false); err != nil { + t.Fatalf("second materializeEdgeletRuntimeTo: %v", err) + } + after, err := os.ReadFile(paths.CertFile) + if err != nil { + t.Fatalf("read cert after second run: %v", err) + } + if len(after) != before { + t.Fatalf("expected existing cert to be preserved") + } +} + +func TestShouldMaterializeEdgeletRuntime(t *testing.T) { + linuxContainer := EdgeletInstallConfig{HostOS: "linux", DeploymentType: "container"} + if !ShouldMaterializeEdgeletRuntime(linuxContainer) { + t.Fatal("linux container deploy should materialize host config") + } + darwinContainer := EdgeletInstallConfig{HostOS: "darwin", DeploymentType: "container", ContainerEngine: "docker"} + if ShouldMaterializeEdgeletRuntime(darwinContainer) { + t.Fatal("darwin container deploy should skip host config materialization") + } +} + +func TestBuildEdgeletBootstrapConfigCommand(t *testing.T) { + engine := "docker" + url := "unix:///var/run/docker.sock" + cfg := EdgeletInstallConfig{ + HostOS: "darwin", + DeploymentType: "container", + ContainerEngine: "docker", + Runtime: &EdgeletRuntimeSpec{ + Arch: "arm64", + Agent: client.AgentConfiguration{ + ContainerEngine: &engine, + ContainerEngineURL: &url, + }, + }, + } + cmd, err := cfg.bootstrapConfigCommand() + if err != nil { + t.Fatalf("bootstrapConfigCommand: %v", err) + } + if !strings.Contains(cmd, "'edgelet' 'config'") && !strings.Contains(cmd, "edgelet config") { + t.Fatalf("expected edgelet config command, got %q", cmd) + } + if !strings.Contains(cmd, "'--ce' 'docker'") && !strings.Contains(cmd, "--ce docker") { + t.Fatalf("expected container engine flag, got %q", cmd) + } + if !strings.Contains(cmd, "'--cu' 'unix:///var/run/docker.sock'") && !strings.Contains(cmd, "--cu unix:///var/run/docker.sock") { + t.Fatalf("expected container engine URL flag, got %q", cmd) + } + if strings.Contains(cmd, "controllerUrl") || strings.Contains(cmd, "controllerCert") { + t.Fatalf("bootstrap config should omit provision-time fields, got %q", cmd) + } + templateOnlyFlags := []string{ + "'--tz'", "'--sec'", "'--cf'", "'--sf'", "'--sd'", + "'--egf'", "'--pf'", "'--uf'", "'--wd'", "'--dev'", + "'--gpsc'", "'--gps'", "'--gpsf'", "'--dt'", + "'--d'", "'--m'", "'--p'", "'--l'", "'--dl'", "'--ld'", + } + for _, bad := range templateOnlyFlags { + if strings.Contains(cmd, bad) { + t.Fatalf("bootstrap config must not apply template defaults, found %q in %q", bad, cmd) + } + } + expectedFlags := []string{"'--ce' 'docker'", "'--cu' 'unix:///var/run/docker.sock'", "'--ft' 'arm64'"} + for _, want := range expectedFlags { + if !strings.Contains(cmd, want) { + t.Fatalf("expected %q in bootstrap config command, got %q", want, cmd) + } + } +} + +func TestBuildEdgeletBootstrapConfigCommandMatchesDeploySpec(t *testing.T) { + engine := "docker" + logLevel := "INFO" + cfg := EdgeletInstallConfig{ + HostOS: "darwin", + DeploymentType: "container", + ContainerEngine: "docker", + Runtime: &EdgeletRuntimeSpec{ + Arch: "arm64", + Agent: client.AgentConfiguration{ + ContainerEngine: &engine, + LogLevel: &logLevel, + }, + }, + } + cmd, err := cfg.bootstrapConfigCommand() + if err != nil { + t.Fatalf("bootstrapConfigCommand: %v", err) + } + for _, want := range []string{ + "'--ll' 'INFO'", + "'--ce' 'docker'", + "'--cu' 'unix:///var/run/docker.sock'", + "'--ft' 'arm64'", + } { + if !strings.Contains(cmd, want) { + t.Fatalf("expected %q in bootstrap config command, got %q", want, cmd) + } + } + for _, absent := range []string{"'--tz'", "'--sec'", "'--cf'", "'--gpsc'"} { + if strings.Contains(cmd, absent) { + t.Fatalf("bootstrap config must not include unset spec field %q, got %q", absent, cmd) + } + } +} + +func TestBuildEdgeletBootstrapConfigCommandSkipsDefaultEdgeletEngine(t *testing.T) { + engine := "edgelet" + cfg := EdgeletInstallConfig{ + HostOS: "darwin", + DeploymentType: "container", + Runtime: &EdgeletRuntimeSpec{ + Agent: client.AgentConfiguration{ + ContainerEngine: &engine, + }, + }, + } + cmd, err := cfg.bootstrapConfigCommand() + if err != nil { + t.Fatalf("bootstrapConfigCommand: %v", err) + } + if cmd != "" { + t.Fatalf("expected empty bootstrap config for default edgelet engine, got %q", cmd) + } +} + +func TestBootstrapEnvDesktopContainer(t *testing.T) { + engine := "docker" + cfg := EdgeletInstallConfig{ + HostOS: "darwin", + DeploymentType: "container", + ContainerEngine: "docker", + Runtime: &EdgeletRuntimeSpec{ + Agent: client.AgentConfiguration{ + ContainerEngine: &engine, + }, + }, + } + env := cfg.bootstrapEnv(true) + if !strings.Contains(env, "EDGELET_BOOTSTRAP_CONFIG_CMD=") { + t.Fatalf("expected bootstrap config env, got %q", env) + } + if !strings.Contains(env, "EDGELET_SCRIPT_STAGE_DIR=") { + t.Fatalf("expected stage dir in bootstrap env, got %q", env) + } + if !strings.Contains(env, "PATH=/tmp/edgelet-scripts/bin:") { + t.Fatalf("expected stage bin on PATH in bootstrap env, got %q", env) + } +} diff --git a/pkg/iofog/install/edgelet_deploy.go b/pkg/iofog/install/edgelet_deploy.go new file mode 100644 index 000000000..84e2f5fbd --- /dev/null +++ b/pkg/iofog/install/edgelet_deploy.go @@ -0,0 +1,102 @@ +package install + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +// DeleteControlPlane removes the edgelet-managed control plane deployment on the local host. +func (agent *LocalEdgelet) DeleteControlPlane() error { + msg := "Deleting edgelet control plane on " + agent.name + cmd := agent.edgeletCommand("edgelet controlplane delete", msg) + if err := agent.runShell(cmd); err != nil && !isEdgeletControlPlaneRemovedError(err) { + return err + } + return nil +} + +// DeployFromFile applies an edgelet manifest (Registry or ControlPlane) on the local host. +func (agent *LocalEdgelet) DeployFromFile(manifestPath string) error { + cmd := fmt.Sprintf("edgelet deploy -f %s", shellQuoteArg(manifestPath)) + return agent.runShell(agent.edgeletCommand(cmd, "Deploying edgelet manifest from "+manifestPath)) +} + +// RegistryList runs edgelet registry ls and returns stdout. +func (agent *LocalEdgelet) RegistryList() (string, error) { + stdout, err := util.Exec("", "sh", "-c", agent.edgeletCommand("edgelet registry ls", "Listing edgelet registries")) + if err != nil { + return "", err + } + return stdout.String(), nil +} + +// WriteTempManifest writes YAML to a temp file for edgelet deploy -f. +// For desktop container edgelet, the file is placed under EdgeletContainerManifestDir +// so docker exec edgelet can read it via the container bind mount. +func WriteTempManifest(data []byte, prefix string, cfg EdgeletInstallConfig) (path string, cleanup func(), err error) { + dir := "" + if IsDesktopContainerDeploy(cfg) { + dir = EdgeletContainerManifestDir + if err := os.MkdirAll(dir, util.DirPerm); err != nil { + return "", nil, err + } + } + + f, err := os.CreateTemp(dir, prefix+"-*.yaml") + if err != nil { + return "", nil, err + } + path = f.Name() + if _, err = f.Write(data); err != nil { + util.IgnoreClose(f) + util.IgnoreErr(os.Remove(path)) + return "", nil, err + } + if err = f.Close(); err != nil { + util.IgnoreErr(os.Remove(path)) + return "", nil, err + } + return path, func() { util.IgnoreErr(os.Remove(path)) }, nil +} + +// WriteDeployManifest writes a manifest using this edgelet's install config. +func (agent *LocalEdgelet) WriteDeployManifest(data []byte, prefix string) (path string, cleanup func(), err error) { + return WriteTempManifest(data, prefix, agent.cfg) +} + +// ParseEdgeletRegistryID finds a registry row matching URL and optional username in edgelet registry ls output. +func ParseEdgeletRegistryID(output, registryURL, username string) (int, error) { + registryURL = strings.TrimSpace(registryURL) + username = strings.TrimSpace(username) + if registryURL == "" { + return 0, fmt.Errorf("registry URL is required") + } + + for i, line := range strings.Split(output, "\n") { + if i == 0 || strings.TrimSpace(line) == "" { + continue + } + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + id, err := strconv.Atoi(fields[0]) + if err != nil { + continue + } + url := fields[1] + user := fields[3] + if url != registryURL { + continue + } + if username != "" && user != username { + continue + } + return id, nil + } + return 0, fmt.Errorf("edgelet registry %q not found in registry ls output", registryURL) +} diff --git a/pkg/iofog/install/edgelet_deploy_test.go b/pkg/iofog/install/edgelet_deploy_test.go new file mode 100644 index 000000000..0c8fe807b --- /dev/null +++ b/pkg/iofog/install/edgelet_deploy_test.go @@ -0,0 +1,72 @@ +package install + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWriteTempManifest_DesktopContainerUsesBindMountDir(t *testing.T) { + t.Parallel() + + cfg := EdgeletInstallConfig{ + HostOS: "darwin", + DeploymentType: "container", + } + data := []byte("kind: ControlPlane\n") + + path, cleanup, err := WriteTempManifest(data, "edgelet-controlplane", cfg) + require.NoError(t, err) + require.NotEmpty(t, cleanup) + t.Cleanup(cleanup) + + require.True(t, strings.HasPrefix(path, EdgeletContainerManifestDir+string(os.PathSeparator)), + "path %q should be under %q", path, EdgeletContainerManifestDir) + + contents, err := os.ReadFile(path) + require.NoError(t, err) + require.Equal(t, data, contents) +} + +func TestWriteTempManifest_NativeUsesSystemTemp(t *testing.T) { + t.Parallel() + + cfg := EdgeletInstallConfig{ + HostOS: "linux", + DeploymentType: "native", + } + data := []byte("kind: ControlPlane\n") + + path, cleanup, err := WriteTempManifest(data, "edgelet-controlplane", cfg) + require.NoError(t, err) + t.Cleanup(cleanup) + + require.False(t, strings.HasPrefix(path, EdgeletContainerManifestDir+string(os.PathSeparator)), + "path %q should not be under container manifest dir", path) + require.NotEqual(t, filepath.Dir(path), EdgeletContainerManifestDir) +} + +func TestParseEdgeletRegistryID(t *testing.T) { + output := `ID URL PUBLIC USERNAME EMAIL +1 docker.io true +2 from_cache true +3 quay.io false john user@domain.com +` + + id, err := ParseEdgeletRegistryID(output, "quay.io", "john") + require.NoError(t, err) + require.Equal(t, 3, id) + + id, err = ParseEdgeletRegistryID(output, "docker.io", "") + require.NoError(t, err) + require.Equal(t, 1, id) + + _, err = ParseEdgeletRegistryID(output, "missing.io", "") + require.Error(t, err) + + _, err = ParseEdgeletRegistryID(output, "quay.io", "wrong-user") + require.Error(t, err) +} diff --git a/pkg/iofog/install/edgelet_local.go b/pkg/iofog/install/edgelet_local.go new file mode 100644 index 000000000..948de75e9 --- /dev/null +++ b/pkg/iofog/install/edgelet_local.go @@ -0,0 +1,284 @@ +package install + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +// LocalEdgelet installs edgelet on the local host using layered scripts. +type LocalEdgelet struct { + defaultAgent + dir string + shareDir string + procs EdgeletProcedures + cfg EdgeletInstallConfig + customInstall bool +} + +func NewLocalEdgelet(name, agentUUID string, cfg EdgeletInstallConfig) (*LocalEdgelet, error) { + stageDir := EdgeletScriptStageDir + procs, err := newDefaultEdgeletProcedures(stageDir, cfg) + if err != nil { + return nil, err + } + return &LocalEdgelet{ + defaultAgent: defaultAgent{name: name, uuid: agentUUID}, + dir: stageDir, + shareDir: EdgeletShareDir(cfg.hostOS()), + procs: procs, + cfg: cfg, + }, nil +} + +func (agent *LocalEdgelet) CustomizeProcedures(dir string, procs *EdgeletProcedures) error { + dir, err := util.FormatPath(dir) + if err != nil { + return err + } + + files, err := os.ReadDir(dir) + if err != nil { + return err + } + for _, file := range files { + if file.IsDir() { + continue + } + procs.scriptNames = append(procs.scriptNames, file.Name()) + content, err := util.ReadFileUnderRoot(dir, file.Name()) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, string(content)) + } + + procs.scriptNames = append(procs.scriptNames, pkg.edgeletScriptPrereq) + prereqContent, err := loadEdgeletScript(pkg.edgeletScriptPrereq) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, prereqContent) + + if procs.Deps.Name == "" { + procs.Deps = agent.procs.Deps + for _, script := range []string{ + pkg.edgeletScriptInstallDeps, + pkg.edgeletScriptConfigureContainerEngine, + } { + procs.scriptNames = append(procs.scriptNames, script) + scriptContent, err := loadEdgeletScript(script) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, scriptContent) + } + } + if procs.Install.Name == "" { + procs.Install = agent.procs.Install + for _, script := range []string{ + pkg.edgeletScriptInstall, + pkg.edgeletScriptInstallContainer, + pkg.edgeletScriptInstallInitUnits, + pkg.edgeletScriptStartEdgelet, + pkg.edgeletScriptConfigureContainerEdgelet, + pkg.edgeletScriptWaitEdgeletReady, + pkg.edgeletScriptBundled, + } { + procs.scriptNames = append(procs.scriptNames, script) + scriptContent, err := loadEdgeletScript(script) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, scriptContent) + } + for _, script := range pkg.edgeletLibScripts { + procs.scriptNames = append(procs.scriptNames, script) + scriptContent, err := loadEdgeletScript(script) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, scriptContent) + } + } else { + agent.customInstall = true + } + if procs.InstallInitUnits.Name == "" { + procs.InstallInitUnits = agent.procs.InstallInitUnits + } + if procs.StartEdgelet.Name == "" { + procs.StartEdgelet = agent.procs.StartEdgelet + } + if procs.ConfigureContainer.Name == "" { + procs.ConfigureContainer = agent.procs.ConfigureContainer + } + if procs.WaitEdgeletReady.Name == "" { + procs.WaitEdgeletReady = agent.procs.WaitEdgeletReady + } + if procs.Bundled.Name == "" { + procs.Bundled = agent.procs.Bundled + } + if procs.Uninstall.Name == "" { + procs.Uninstall = agent.procs.Uninstall + procs.scriptNames = append(procs.scriptNames, pkg.edgeletScriptUninstall) + scriptContent, err := loadEdgeletScript(pkg.edgeletScriptUninstall) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, scriptContent) + } + + agent.bindProcedurePaths(procs, agent.dir) + agent.procs = *procs + return nil +} + +func (agent *LocalEdgelet) bindProcedurePaths(procs *EdgeletProcedures, stageDir string) { + procs.check.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptPrereq) + procs.DetectInit.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptDetectInit) + procs.Deps.destPath = util.JoinAgentPath(stageDir, procs.Deps.Name) + procs.refreshInstallEntry(stageDir, agent.cfg) + procs.InstallInitUnits.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptInstallInitUnits) + procs.InstallContainer.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptInstallContainer) + procs.StartEdgelet.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptStartEdgelet) + procs.ConfigureContainer.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptConfigureContainerEdgelet) + procs.WaitEdgeletReady.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptWaitEdgeletReady) + procs.Bundled.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptBundled) + procs.Uninstall.destPath = util.JoinAgentPath(stageDir, procs.Uninstall.Name) +} + +func (agent *LocalEdgelet) SetVersion(version string) error { + if version == "" || agent.customInstall { + return nil + } + agent.cfg.Version = version + return agent.procs.setInstallArgs(agent.cfg) +} + +func (agent *LocalEdgelet) SetContainerImage(image string) error { + if image == "" || agent.customInstall { + return nil + } + agent.cfg.ContainerImage = image + return agent.procs.setInstallArgs(agent.cfg) +} + +func (agent *LocalEdgelet) SetAirgap(binPath string) error { + agent.cfg.Airgap = true + agent.cfg.BinPath = binPath + return agent.procs.setInstallArgs(agent.cfg) +} + +func (agent *LocalEdgelet) Bootstrap() error { + if err := agent.materializeScripts(); err != nil { + return err + } + useSudo := needsLocalSudo(agent.cfg) + for _, cmd := range agent.procs.preInstallCommands(agent.name, agent.cfg, useSudo) { + Verbose(cmd.msg) + if err := agent.runShell(cmd.cmd); err != nil { + return err + } + } + if ShouldMaterializeEdgeletRuntime(agent.cfg) { + if err := MaterializeEdgeletRuntime(agent.cfg.hostOS(), agent.cfg.Runtime); err != nil { + return err + } + } + for _, cmd := range agent.procs.postInstallCommands(agent.name, agent.cfg, useSudo) { + Verbose(cmd.msg) + if err := agent.runShell(cmd.cmd); err != nil { + return err + } + } + return nil +} + +func (agent *LocalEdgelet) Deprovision() error { + cmd := agent.edgeletCommand("edgelet deprovision", "Deprovisioning edgelet on "+agent.name) + if err := agent.runShell(cmd); err != nil && !isEdgeletNotProvisionedError(err) { + return err + } + return nil +} + +func (agent *LocalEdgelet) Prune() error { + return agent.runShell(agent.edgeletCommand("edgelet system prune", "Pruning edgelet on "+agent.name)) +} + +func (agent *LocalEdgelet) Uninstall(removeData bool) error { + if err := agent.materializeScripts(); err != nil { + return err + } + agent.procs.setUninstallArgs(agent.cfg, removeData) + cmd := agent.procs.Uninstall.getCommand() + if needsLocalSudo(agent.cfg) { + cmd = "sudo env PATH=$PATH:/usr/local/bin:/usr/bin:/sbin " + cmd + } + msg := "Removing edgelet from " + agent.name + Verbose(msg) + return agent.runShell(agent.cfg.bootstrapEnv(true) + " " + cmd) +} + +func (agent *LocalEdgelet) edgeletCommand(cmd, msg string) string { + prefix := "" + if needsLocalSudo(agent.cfg) { + prefix = "sudo env PATH=$PATH:/usr/local/bin:/usr/bin:/sbin " + } + Verbose(msg) + return agent.cfg.bootstrapEnv(true) + " " + prefix + cmd +} + +func (agent *LocalEdgelet) Configure(controllerEndpoint string, user IofogUser, sdkOpt client.Options) (string, error) { + key, caCert, err := agent.getProvisionKey(controllerEndpoint, user, sdkOpt) + if err != nil { + return "", err + } + cmds, err := EdgeletProvisionCommands(controllerEndpoint, key, caCert, needsLocalSudo(agent.cfg)) + if err != nil { + return "", err + } + for _, cmd := range cmds { + Verbose(cmd.msg) + if err := agent.runShell(agent.edgeletCommand(cmd.cmd, cmd.msg)); err != nil { + return "", err + } + } + return agent.uuid, nil +} + +func needsLocalSudo(cfg EdgeletInstallConfig) bool { + return cfg.native() +} + +func (agent *LocalEdgelet) materializeScripts() error { + if err := os.MkdirAll(agent.dir, util.DirPerm); err != nil { + return err + } + if err := os.MkdirAll(filepath.Join(agent.dir, "lib"), util.DirPerm); err != nil { + return err + } + for idx, script := range agent.procs.scriptNames { + path := filepath.Join(agent.dir, script) + if err := os.MkdirAll(filepath.Dir(path), util.DirPerm); err != nil { + return err + } + if err := os.WriteFile(path, []byte(agent.procs.scriptContents[idx]), util.ExecPerm); err != nil { // #nosec G306 -- executable install scripts + return err + } + } + return nil +} + +func (agent *LocalEdgelet) runShell(command string) error { + if strings.TrimSpace(command) == "" { + return fmt.Errorf("empty command") + } + // Match remote SSH: full command string runs under a shell so shellJoinArgs quotes work. + _, err := util.Exec("", "sh", "-c", command) + return err +} diff --git a/pkg/iofog/install/edgelet_local_test.go b/pkg/iofog/install/edgelet_local_test.go new file mode 100644 index 000000000..506d82a79 --- /dev/null +++ b/pkg/iofog/install/edgelet_local_test.go @@ -0,0 +1,47 @@ +package install + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func TestLocalEdgeletRunShellHonorsShellQuotes(t *testing.T) { + dir := t.TempDir() + script := filepath.Join(dir, "echoarg.sh") + if err := os.WriteFile(script, []byte("#!/bin/sh\necho \"$1\"\n"), 0o755); err != nil { + t.Fatalf("write script: %v", err) + } + + // runShell delegates to sh -c; quoted args must not reach argv with literal quotes. + out, err := util.Exec("", "sh", "-c", script+" 'docker'") + if err != nil { + t.Fatalf("Exec: %v", err) + } + if got := strings.TrimSpace(out.String()); got != "docker" { + t.Fatalf("got %q, want docker", got) + } +} + +func TestLocalEdgeletPreInstallDepsCommandQuoted(t *testing.T) { + cfg := EdgeletInstallConfig{ + HostOS: "darwin", + ContainerEngine: "docker", + DeploymentType: "native", + } + procs, err := newDefaultEdgeletProcedures(EdgeletScriptStageDir, cfg) + if err != nil { + t.Fatalf("newDefaultEdgeletProcedures: %v", err) + } + pre := procs.preInstallCommands("edge-2", cfg, false) + if len(pre) < 3 { + t.Fatalf("expected pre-install commands, got %d", len(pre)) + } + depsCmd := pre[2].cmd + if !strings.Contains(depsCmd, "install_deps.sh 'docker' 'native'") { + t.Fatalf("expected shell-quoted deps args, got %q", depsCmd) + } +} diff --git a/pkg/iofog/install/edgelet_procedures_test.go b/pkg/iofog/install/edgelet_procedures_test.go new file mode 100644 index 000000000..105b9ec3d --- /dev/null +++ b/pkg/iofog/install/edgelet_procedures_test.go @@ -0,0 +1,190 @@ +package install + +import ( + "os" + "path" + "strings" + "testing" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func TestDefaultEdgeletProceduresScripts(t *testing.T) { + util.SetEdgeletReleaseBaseForTest("https://github.com/Datasance/edgelet/releases/download") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") + t.Cleanup(func() { + util.ResetEdgeletReleaseBaseForTest() + util.ResetEdgeletBinaryVersionForTest() + }) + + cfg := EdgeletInstallConfig{ + HostOS: "linux", + Arch: "amd64", + ContainerEngine: "edgelet", + DeploymentType: "native", + } + procs, err := newDefaultEdgeletProcedures(EdgeletScriptStageDir, cfg) + if err != nil { + t.Fatalf("newDefaultEdgeletProcedures: %v", err) + } + + names := edgeletScriptNames() + if len(procs.scriptNames) != len(names) { + t.Fatalf("expected %d embedded scripts, got %d", len(names), len(procs.scriptNames)) + } + if len(procs.scriptContents) != len(procs.scriptNames) { + t.Fatalf("script content count mismatch") + } + if procs.Deps.Args[0] != "edgelet" { + t.Fatalf("deps args = %v, want edgelet engine", procs.Deps.Args) + } + joined := strings.Join(procs.Install.Args, " ") + if !strings.Contains(joined, "--version=v1.0.0-rc.6") { + t.Fatalf("expected --version flag in install args, got %v", procs.Install.Args) + } + if !strings.Contains(joined, "--skip-start") { + t.Fatalf("expected --skip-start in install args, got %v", procs.Install.Args) + } +} + +func TestEdgeletInstallFlagsContainer(t *testing.T) { + cfg := EdgeletInstallConfig{ + DeploymentType: "container", + ContainerEngine: "docker", + ContainerImage: "ghcr.io/example/edgelet:1.2.3", + TimeZone: "Europe/Istanbul", + } + flags, err := cfg.installFlags() + if err != nil { + t.Fatalf("installFlags: %v", err) + } + joined := strings.Join(flags, " ") + if !strings.Contains(joined, "--image=ghcr.io/example/edgelet:1.2.3") { + t.Fatalf("unexpected container install flags: %v", flags) + } + if !strings.Contains(joined, "--engine=docker") { + t.Fatalf("unexpected container install flags: %v", flags) + } +} + +func TestEdgeletBootstrapEnvContainerEngineURL(t *testing.T) { + cfg := EdgeletInstallConfig{ + ContainerEngine: "podman", + DeploymentType: "container", + } + env := cfg.bootstrapEnv(true) + if !strings.Contains(env, "EDGELET_CONTAINER_ENGINE_URL=unix:///run/podman/podman.sock") { + t.Fatalf("bootstrap env missing podman socket URL: %q", env) + } +} + +func TestCustomizeEdgeletProceduresPartial(t *testing.T) { + dir := t.TempDir() + srcDir := "../../../assets/edgelet/scripts" + if err := copyDir(srcDir, dir); err != nil { + t.Fatalf("copyDir: %v", err) + } + if err := os.Remove(path.Join(dir, pkg.edgeletScriptPrereq)); err != nil { + t.Fatalf("remove prereq: %v", err) + } + if err := os.Remove(path.Join(dir, pkg.edgeletScriptInstall)); err != nil { + t.Fatalf("remove install script: %v", err) + } + + agent := &RemoteEdgelet{dir: EdgeletScriptStageDir} + procs := EdgeletProcedures{ + AgentProcedures: AgentProcedures{ + Deps: Entrypoint{Name: pkg.edgeletScriptInstallDeps}, + Uninstall: Entrypoint{ + Name: pkg.edgeletScriptUninstall, + }, + }, + } + if err := agent.CustomizeProcedures(dir, &procs); err != nil { + t.Fatalf("CustomizeProcedures: %v", err) + } + if agent.customInstall { + t.Fatalf("expected default install script to be embedded") + } + if len(procs.scriptNames) < len(edgeletScriptNames()) { + t.Fatalf("expected embedded defaults to be appended, got %v", procs.scriptNames) + } +} + +func TestInstallDepsSkipMatrix(t *testing.T) { + tests := []struct { + engine string + deploy string + skip bool + }{ + {"edgelet", "native", true}, + {"edgelet", "container", true}, + {"docker", "native", false}, + {"podman", "container", false}, + } + for _, tt := range tests { + cfg := EdgeletInstallConfig{ContainerEngine: tt.engine, DeploymentType: tt.deploy} + if got := util.ShouldSkipInstallDeps(cfg.containerEngine(), cfg.deploymentType()); got != tt.skip { + t.Fatalf("engine=%q deploy=%q skip=%v want %v", tt.engine, tt.deploy, got, tt.skip) + } + } +} + +func TestBootstrapCommandGeneration(t *testing.T) { + util.SetEdgeletReleaseBaseForTest("https://example.com/download") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") + + cfg := EdgeletInstallConfig{HostOS: "linux", Arch: "amd64", ContainerEngine: "docker"} + procs, err := newDefaultEdgeletProcedures(EdgeletScriptStageDir, cfg) + if err != nil { + t.Fatalf("newDefaultEdgeletProcedures: %v", err) + } + pre := procs.preInstallCommands("edge-node", cfg, true) + if len(pre) != 4 { + t.Fatalf("expected 4 pre-install commands, got %d", len(pre)) + } + post := procs.postInstallCommands("edge-node", cfg, true) + if len(post) != 5 { + t.Fatalf("expected 5 post-install commands, got %d", len(post)) + } + if !strings.Contains(pre[0].cmd, "CONTAINER_ENGINE=docker") { + t.Fatalf("expected bootstrap env injection, got %q", pre[0].cmd) + } + if !strings.Contains(pre[3].cmd, "install.sh") { + t.Fatalf("expected install.sh command, got %q", pre[3].cmd) + } + if !strings.Contains(pre[3].cmd, "sudo env ") || !strings.Contains(pre[3].cmd, "CONTAINER_ENGINE=docker") { + t.Fatalf("expected sudo env bootstrap for install, got %q", pre[3].cmd) + } + if !strings.Contains(post[0].cmd, "sudo env ") || !strings.Contains(post[0].cmd, "CONTAINER_ENGINE=docker") { + t.Fatalf("expected sudo env bootstrap for post-install, got %q", post[0].cmd) + } +} + +func TestEdgeletUninstallArgs(t *testing.T) { + cfg := EdgeletInstallConfig{} + procs, err := newDefaultEdgeletProcedures(EdgeletScriptStageDir, cfg) + if err != nil { + t.Fatalf("newDefaultEdgeletProcedures: %v", err) + } + procs.setUninstallArgs(cfg, true) + if len(procs.Uninstall.Args) != 1 || procs.Uninstall.Args[0] != "--remove-data" { + t.Fatalf("unexpected uninstall args with remove-data: %v", procs.Uninstall.Args) + } + procs.setUninstallArgs(cfg, false) + if len(procs.Uninstall.Args) != 0 { + t.Fatalf("expected no uninstall args without remove-data, got %v", procs.Uninstall.Args) + } +} + +func TestEdgeletShareDir(t *testing.T) { + if got := EdgeletShareDir("linux"); got != "/usr/share/edgelet" { + t.Fatalf("linux share dir = %q", got) + } + if got := EdgeletShareDir("darwin"); got != "/usr/local/share/edgelet" { + t.Fatalf("darwin share dir = %q", got) + } + if got := EdgeletShareDir("windows"); got != `%ProgramData%\Edgelet\scripts` { + t.Fatalf("windows share dir = %q", got) + } +} diff --git a/pkg/iofog/install/edgelet_remote.go b/pkg/iofog/install/edgelet_remote.go new file mode 100644 index 000000000..b9e65a7fd --- /dev/null +++ b/pkg/iofog/install/edgelet_remote.go @@ -0,0 +1,512 @@ +package install + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const remoteEdgeletManifestDir = "/tmp" + +// remoteEdgeletRunHook is set by tests to mock SSH command execution. +var remoteEdgeletRunHook func(agent *RemoteEdgelet, cmds []command) error + +// remoteEdgeletInstallFileHook is set by tests to mock remote config/cert writes. +var remoteEdgeletInstallFileHook func(agent *RemoteEdgelet, destPath string, content []byte, perm string) error + +// RemoteEdgelet installs edgelet on a remote host over SSH using layered scripts. +type RemoteEdgelet struct { + defaultAgent + ssh *util.SecureShellClient + dir string + shareDir string + procs EdgeletProcedures + cfg EdgeletInstallConfig + customInstall bool +} + +func NewRemoteEdgelet(user, host string, port int, privKeyFilename, agentName, agentUUID string, cfg EdgeletInstallConfig) (*RemoteEdgelet, error) { + ssh, err := util.NewSecureShellClient(user, host, privKeyFilename) + if err != nil { + return nil, err + } + ssh.SetPort(port) + + stageDir := EdgeletScriptStageDir + procs, err := newDefaultEdgeletProcedures(stageDir, cfg) + if err != nil { + return nil, err + } + + return &RemoteEdgelet{ + defaultAgent: defaultAgent{name: agentName, uuid: agentUUID}, + ssh: ssh, + dir: stageDir, + shareDir: EdgeletShareDir(cfg.hostOS()), + procs: procs, + cfg: cfg, + }, nil +} + +func (agent *RemoteEdgelet) CustomizeProcedures(dir string, procs *EdgeletProcedures) error { + dir, err := util.FormatPath(dir) + if err != nil { + return err + } + + files, err := os.ReadDir(dir) + if err != nil { + return err + } + for _, file := range files { + if file.IsDir() { + continue + } + procs.scriptNames = append(procs.scriptNames, file.Name()) + content, err := util.ReadFileUnderRoot(dir, file.Name()) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, string(content)) + } + + procs.scriptNames = append(procs.scriptNames, pkg.edgeletScriptPrereq) + prereqContent, err := loadEdgeletScript(pkg.edgeletScriptPrereq) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, prereqContent) + + if procs.Deps.Name == "" { + procs.Deps = agent.procs.Deps + for _, script := range []string{ + pkg.edgeletScriptInstallDeps, + pkg.edgeletScriptConfigureContainerEngine, + } { + procs.scriptNames = append(procs.scriptNames, script) + scriptContent, err := loadEdgeletScript(script) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, scriptContent) + } + } + if procs.Install.Name == "" { + procs.Install = agent.procs.Install + for _, script := range []string{ + pkg.edgeletScriptInstall, + pkg.edgeletScriptInstallContainer, + pkg.edgeletScriptInstallInitUnits, + pkg.edgeletScriptStartEdgelet, + pkg.edgeletScriptConfigureContainerEdgelet, + pkg.edgeletScriptWaitEdgeletReady, + pkg.edgeletScriptBundled, + } { + procs.scriptNames = append(procs.scriptNames, script) + scriptContent, err := loadEdgeletScript(script) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, scriptContent) + } + for _, script := range pkg.edgeletLibScripts { + procs.scriptNames = append(procs.scriptNames, script) + scriptContent, err := loadEdgeletScript(script) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, scriptContent) + } + } else { + agent.customInstall = true + } + if procs.InstallInitUnits.Name == "" { + procs.InstallInitUnits = agent.procs.InstallInitUnits + } + if procs.StartEdgelet.Name == "" { + procs.StartEdgelet = agent.procs.StartEdgelet + } + if procs.ConfigureContainer.Name == "" { + procs.ConfigureContainer = agent.procs.ConfigureContainer + } + if procs.WaitEdgeletReady.Name == "" { + procs.WaitEdgeletReady = agent.procs.WaitEdgeletReady + } + if procs.Bundled.Name == "" { + procs.Bundled = agent.procs.Bundled + } + if procs.Uninstall.Name == "" { + procs.Uninstall = agent.procs.Uninstall + procs.scriptNames = append(procs.scriptNames, pkg.edgeletScriptUninstall) + scriptContent, err := loadEdgeletScript(pkg.edgeletScriptUninstall) + if err != nil { + return err + } + procs.scriptContents = append(procs.scriptContents, scriptContent) + } + + agent.bindProcedurePaths(procs, agent.dir) + agent.procs = *procs + return nil +} + +func (agent *RemoteEdgelet) bindProcedurePaths(procs *EdgeletProcedures, stageDir string) { + procs.check.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptPrereq) + procs.DetectInit.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptDetectInit) + procs.Deps.destPath = util.JoinAgentPath(stageDir, procs.Deps.Name) + procs.refreshInstallEntry(stageDir, agent.cfg) + procs.InstallInitUnits.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptInstallInitUnits) + procs.InstallContainer.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptInstallContainer) + procs.StartEdgelet.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptStartEdgelet) + procs.ConfigureContainer.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptConfigureContainerEdgelet) + procs.WaitEdgeletReady.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptWaitEdgeletReady) + procs.Bundled.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptBundled) + procs.Uninstall.destPath = util.JoinAgentPath(stageDir, procs.Uninstall.Name) +} + +func (agent *RemoteEdgelet) SetVersion(version string) error { + if version == "" || agent.customInstall { + return nil + } + agent.cfg.Version = version + return agent.procs.setInstallArgs(agent.cfg) +} + +func (agent *RemoteEdgelet) SetContainerImage(image string) error { + if image == "" || agent.customInstall { + return nil + } + agent.cfg.ContainerImage = image + return agent.procs.setInstallArgs(agent.cfg) +} + +func (agent *RemoteEdgelet) SetAirgap(binPath string) error { + agent.cfg.Airgap = true + agent.cfg.BinPath = binPath + return agent.procs.setInstallArgs(agent.cfg) +} + +func (agent *RemoteEdgelet) detectAndSetHostOS() error { + if remoteEdgeletRunHook != nil { + return nil + } + if err := agent.ssh.Connect(); err != nil { + return err + } + defer util.Log(agent.ssh.Disconnect) + + out, err := agent.ssh.Run("uname -s") + if err != nil { + return fmt.Errorf("detect remote host OS: %w", err) + } + raw := strings.TrimSpace(out.String()) + osName, err := util.NormalizeEdgeletOS(raw) + if err != nil { + return fmt.Errorf("normalize remote host OS %q: %w", raw, err) + } + agent.cfg.HostOS = osName + agent.shareDir = EdgeletShareDir(osName) + agent.bindProcedurePaths(&agent.procs, agent.dir) + return agent.procs.setInstallArgs(agent.cfg) +} + +func (agent *RemoteEdgelet) Bootstrap() error { + if err := agent.detectAndSetHostOS(); err != nil { + return err + } + if err := agent.copyInstallScripts(); err != nil { + return err + } + if err := agent.run(agent.procs.preInstallCommands(agent.name, agent.cfg, true)); err != nil { + return err + } + if err := agent.materializeRuntimeConfig(); err != nil { + return err + } + return agent.run(agent.procs.postInstallCommands(agent.name, agent.cfg, true)) +} + +func (agent *RemoteEdgelet) Configure(controllerEndpoint string, user IofogUser, sdkOpt client.Options) (string, error) { + key, caCert, err := agent.getProvisionKey(controllerEndpoint, user, sdkOpt) + if err != nil { + return "", err + } + cmds, err := EdgeletProvisionCommands(controllerEndpoint, key, caCert, true) + if err != nil { + return "", err + } + if err := agent.run(cmds); err != nil { + return "", err + } + return agent.uuid, nil +} + +func (agent *RemoteEdgelet) materializeRuntimeConfig() error { + if !ShouldMaterializeEdgeletRuntime(agent.cfg) { + return nil + } + paths := EdgeletPaths(agent.cfg.hostOS()) + if err := agent.run([]command{{ + cmd: fmt.Sprintf("sudo mkdir -p %s && sudo chmod 755 %s", paths.ConfigDir, paths.ConfigDir), + msg: "Creating edgelet config directory on " + agent.name, + }}); err != nil { + return err + } + + configYAML, err := buildRemoteEdgeletConfigYAML(agent) + if err != nil { + return err + } + if err := agent.installRemoteFileIfMissing(paths.ConfigFile, configYAML, "640"); err != nil { + return fmt.Errorf("write edgelet config: %w", err) + } + + sampleCA, err := loadEdgeletSampleCA() + if err != nil { + return err + } + if err := agent.installRemoteFileIfMissing(paths.CertFile, sampleCA, "644"); err != nil { + return fmt.Errorf("write edgelet sample CA: %w", err) + } + return nil +} + +func buildRemoteEdgeletConfigYAML(agent *RemoteEdgelet) ([]byte, error) { + spec := agent.cfg.Runtime + var agentCfg *client.AgentConfiguration + arch := agent.cfg.Arch + latitude := 0.0 + longitude := 0.0 + if spec != nil { + agentCfg = &spec.Agent + latitude = spec.Latitude + longitude = spec.Longitude + if spec.Arch != "" { + arch = spec.Arch + } + } + return BuildEdgeletConfigYAML(agent.cfg.hostOS(), arch, latitude, longitude, agentCfg) +} + +func (agent *RemoteEdgelet) installRemoteFileIfMissing(destPath string, content []byte, perm string) error { + if remoteEdgeletInstallFileHook != nil { + return remoteEdgeletInstallFileHook(agent, destPath, content, perm) + } + if err := agent.ssh.Connect(); err != nil { + return err + } + defer util.Log(agent.ssh.Disconnect) + + checkCmd := fmt.Sprintf("test -f %q", destPath) + if _, err := agent.ssh.Run(checkCmd); err == nil { + return nil + } + + tmpName := filepath.Base(destPath) + ".tmp" + reader := strings.NewReader(string(content)) + if err := agent.ssh.CopyTo(reader, "/tmp", tmpName, "0644", int64(len(content))); err != nil { + return err + } + installCmd := fmt.Sprintf("sudo install -m %s /tmp/%s %s", perm, tmpName, destPath) + if _, err := agent.ssh.Run(installCmd); err != nil { + return err + } + _, err := agent.ssh.Run(fmt.Sprintf("rm -f /tmp/%s", tmpName)) + return err +} + +// DeployFromFile applies an edgelet manifest (Registry or ControlPlane) on the remote host. +func (agent *RemoteEdgelet) DeployFromFile(manifestPath string) error { + cmd := fmt.Sprintf("sudo edgelet deploy -f %s", shellQuoteArg(manifestPath)) + return agent.run([]command{{ + cmd: cmd, + msg: "Deploying edgelet manifest from " + manifestPath, + }}) +} + +// RegistryList runs edgelet registry ls on the remote host and returns stdout. +func (agent *RemoteEdgelet) RegistryList() (string, error) { + if remoteEdgeletRunHook != nil { + return "", nil + } + if err := agent.ssh.Connect(); err != nil { + return "", err + } + defer util.Log(agent.ssh.Disconnect) + + out, err := agent.ssh.Run("sudo edgelet registry ls") + if err != nil { + return "", err + } + return out.String(), nil +} + +// WriteDeployManifest writes manifest bytes to a remote temp file for edgelet deploy -f. +func (agent *RemoteEdgelet) WriteDeployManifest(data []byte, prefix string) (path string, cleanup func(), err error) { + localPath, localCleanup, err := WriteTempManifest(data, prefix, agent.cfg) + if err != nil { + return "", nil, err + } + defer localCleanup() + + remotePath := fmt.Sprintf("%s/edgelet-%s.yaml", remoteEdgeletManifestDir, prefix) + if err := agent.copyLocalFileToRemote(localPath, remotePath); err != nil { + return "", nil, err + } + cleanup = func() { + _ = agent.run([]command{{ + cmd: fmt.Sprintf("rm -f %s", shellQuoteArg(remotePath)), + msg: "Removing edgelet manifest on " + agent.name, + }}) + } + return remotePath, cleanup, nil +} + +func (agent *RemoteEdgelet) copyLocalFileToRemote(localPath, remotePath string) error { + if remoteEdgeletRunHook != nil { + return nil + } + content, err := util.ReadValidatedFile(localPath) + if err != nil { + return err + } + if err := agent.ssh.Connect(); err != nil { + return err + } + defer util.Log(agent.ssh.Disconnect) + + tmpName := filepath.Base(remotePath) + ".upload" + reader := strings.NewReader(string(content)) + if err := agent.ssh.CopyTo(reader, remoteEdgeletManifestDir, tmpName, "0644", int64(len(content))); err != nil { + return err + } + installCmd := fmt.Sprintf("sudo install -m 644 %s/%s %s", remoteEdgeletManifestDir, tmpName, remotePath) + if _, err := agent.ssh.Run(installCmd); err != nil { + return err + } + _, err = agent.ssh.Run(fmt.Sprintf("rm -f %s/%s", remoteEdgeletManifestDir, tmpName)) + return err +} + +func (agent *RemoteEdgelet) Deprovision() error { + cmds := []command{{ + cmd: "sudo edgelet deprovision", + msg: "Deprovisioning edgelet on " + agent.name, + }} + if err := agent.run(cmds); err != nil && !isEdgeletNotProvisionedError(err) { + return err + } + return nil +} + +func (agent *RemoteEdgelet) DeleteControlPlane() error { + cmds := []command{{ + cmd: "sudo edgelet controlplane delete", + msg: "Deleting edgelet control plane on " + agent.name, + }} + if err := agent.run(cmds); err != nil && !isEdgeletControlPlaneRemovedError(err) { + return err + } + return nil +} + +func (agent *RemoteEdgelet) Prune() error { + cmds := []command{{ + cmd: "sudo edgelet system prune", + msg: "Pruning edgelet on " + agent.name, + }} + return agent.run(cmds) +} + +func (agent *RemoteEdgelet) Uninstall(removeData bool) error { + if err := agent.detectAndSetHostOS(); err != nil { + return err + } + if err := agent.copyInstallScripts(); err != nil { + return err + } + agent.procs.setUninstallArgs(agent.cfg, removeData) + cmds := []command{ + {cmd: "sudo " + agent.procs.Uninstall.getCommand(), msg: "Removing edgelet from " + agent.name}, + } + return agent.run(cmds) +} + +func (agent *RemoteEdgelet) run(cmds []command) error { + if remoteEdgeletRunHook != nil { + return remoteEdgeletRunHook(agent, cmds) + } + if err := agent.ssh.Connect(); err != nil { + return err + } + defer util.Log(agent.ssh.Disconnect) + + for _, cmd := range cmds { + Verbose(cmd.msg) + if _, err := agent.ssh.Run(cmd.cmd); err != nil { + return err + } + } + return nil +} + +func (agent *RemoteEdgelet) copyInstallScripts() error { + Verbose("Copying edgelet install scripts to " + agent.name) + stage := agent.dir + cmds := []command{ + { + cmd: fmt.Sprintf("sudo mkdir -p %s %s/lib && sudo chmod -R 755 %s", stage, stage, stage), + msg: "Creating edgelet script staging directory", + }, + } + if err := agent.run(cmds); err != nil { + return err + } + if remoteEdgeletRunHook != nil { + return nil + } + + if err := agent.ssh.Connect(); err != nil { + return err + } + defer util.Log(agent.ssh.Disconnect) + + for idx, script := range agent.procs.scriptNames { + if err := agent.installRemoteScript(stage, script, agent.procs.scriptContents[idx]); err != nil { + return err + } + } + return nil +} + +func edgeletScriptTmpName(relPath string) string { + safe := strings.ReplaceAll(relPath, "/", "-") + return "edgelet-" + safe + ".tmp" +} + +func (agent *RemoteEdgelet) installRemoteScript(stageDir, relPath, content string) error { + destPath := util.JoinAgentPath(stageDir, relPath) + if strings.Contains(relPath, "/") { + destDir := stageDir + "/" + filepath.Dir(relPath) + mkdirCmd := fmt.Sprintf("sudo mkdir -p %s && sudo chmod 755 %s", destDir, destDir) + if _, err := agent.ssh.Run(mkdirCmd); err != nil { + return err + } + } + + tmpName := edgeletScriptTmpName(relPath) + reader := strings.NewReader(content) + if err := agent.ssh.CopyTo(reader, "/tmp", tmpName, "0644", int64(len(content))); err != nil { + return err + } + installCmd := fmt.Sprintf("sudo install -m 755 /tmp/%s %s", tmpName, destPath) + if _, err := agent.ssh.Run(installCmd); err != nil { + return err + } + _, err := agent.ssh.Run(fmt.Sprintf("rm -f /tmp/%s", tmpName)) + return err +} diff --git a/pkg/iofog/install/edgelet_remote_test.go b/pkg/iofog/install/edgelet_remote_test.go new file mode 100644 index 000000000..7835969a6 --- /dev/null +++ b/pkg/iofog/install/edgelet_remote_test.go @@ -0,0 +1,104 @@ +package install + +import ( + "strings" + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func TestRemoteEdgeletBootstrapUsesMockedSSH(t *testing.T) { + util.SetEdgeletReleaseBaseForTest("https://example.com/download") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") + t.Cleanup(func() { + util.ResetEdgeletReleaseBaseForTest() + util.ResetEdgeletBinaryVersionForTest() + }) + + var ran []string + remoteEdgeletRunHook = func(agent *RemoteEdgelet, cmds []command) error { + for _, cmd := range cmds { + ran = append(ran, cmd.cmd) + } + return nil + } + t.Cleanup(func() { remoteEdgeletRunHook = nil }) + remoteEdgeletInstallFileHook = func(*RemoteEdgelet, string, []byte, string) error { return nil } + t.Cleanup(func() { remoteEdgeletInstallFileHook = nil }) + + cfg := EdgeletInstallConfig{ + HostOS: "linux", + Arch: "amd64", + ContainerEngine: "edgelet", + DeploymentType: "native", + } + procs, err := newDefaultEdgeletProcedures(EdgeletScriptStageDir, cfg) + if err != nil { + t.Fatalf("newDefaultEdgeletProcedures: %v", err) + } + agent := &RemoteEdgelet{ + defaultAgent: defaultAgent{name: "edge-node"}, + dir: EdgeletScriptStageDir, + procs: procs, + cfg: cfg, + } + + if err := agent.Bootstrap(); err != nil { + t.Fatalf("Bootstrap: %v", err) + } + if len(ran) < 8 { + t.Fatalf("expected bootstrap commands, got %d: %v", len(ran), ran) + } + + joined := strings.Join(ran, " ") + if !strings.Contains(joined, "install.sh") { + t.Fatalf("expected install.sh in bootstrap, got: %v", ran) + } + if !strings.Contains(joined, "wait_edgelet_ready.sh") { + t.Fatalf("expected wait_edgelet_ready.sh in bootstrap, got: %v", ran) + } + if !strings.Contains(joined, "/etc/edgelet") { + t.Fatalf("expected runtime config materialization, got: %v", ran) + } +} + +func TestRemoteEdgeletConfigureUsesMockedSSH(t *testing.T) { + var ran []string + remoteEdgeletRunHook = func(agent *RemoteEdgelet, cmds []command) error { + for _, cmd := range cmds { + ran = append(ran, cmd.cmd) + } + return nil + } + t.Cleanup(func() { remoteEdgeletRunHook = nil }) + + agent := &RemoteEdgelet{ + defaultAgent: defaultAgent{name: "edge-node", uuid: "agent-uuid"}, + cfg: EdgeletInstallConfig{DeploymentType: "native"}, + } + + getProvisionKeyHook = func(*defaultAgent, string, IofogUser, client.Options) (string, string, error) { + return "provision-key", "base64-ca", nil + } + t.Cleanup(func() { getProvisionKeyHook = nil }) + + if _, err := agent.Configure("https://controller.example.com", IofogUser{}, client.Options{}); err != nil { + t.Fatalf("Configure: %v", err) + } + if len(ran) != 3 { + t.Fatalf("expected 3 edgelet commands, got %d: %v", len(ran), ran) + } + for _, want := range []string{"edgelet config --a", "edgelet config cert", "edgelet provision"} { + found := false + for _, cmd := range ran { + if strings.Contains(cmd, want) { + found = true + break + } + } + if !found { + t.Fatalf("missing %q in commands: %v", want, ran) + } + } +} diff --git a/pkg/iofog/install/edgelet_scripts.go b/pkg/iofog/install/edgelet_scripts.go new file mode 100644 index 000000000..a79d401ea --- /dev/null +++ b/pkg/iofog/install/edgelet_scripts.go @@ -0,0 +1,394 @@ +package install + +import ( + "fmt" + "os" + "strings" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +// EdgeletInstallConfig drives layered install script arguments. +type EdgeletInstallConfig struct { + HostOS string + Version string + Arch string + ContainerEngine string + DeploymentType string + ContainerImage string + TimeZone string + BinPath string + Airgap bool + Runtime *EdgeletRuntimeSpec +} + +func (cfg EdgeletInstallConfig) native() bool { + return cfg.DeploymentType == "" || cfg.DeploymentType == "native" +} + +func (cfg EdgeletInstallConfig) containerEngine() string { + if cfg.ContainerEngine == "" { + return "edgelet" + } + return cfg.ContainerEngine +} + +func (cfg EdgeletInstallConfig) deploymentType() string { + if cfg.DeploymentType == "" { + return "native" + } + return cfg.DeploymentType +} + +func (cfg EdgeletInstallConfig) version() string { + if cfg.Version != "" { + return cfg.Version + } + return util.GetEdgeletBinaryVersion() +} + +func (cfg EdgeletInstallConfig) installModeEnv() string { + if cfg.native() { + return "native" + } + return "container" +} + +func (cfg EdgeletInstallConfig) containerImage() string { + if cfg.ContainerImage != "" { + return cfg.ContainerImage + } + return util.GetEdgeletImage() +} + +func (cfg EdgeletInstallConfig) timeZone() string { + if cfg.TimeZone != "" { + return cfg.TimeZone + } + return "UTC" +} + +func (cfg EdgeletInstallConfig) depsArgs() []string { + return []string{cfg.containerEngine(), cfg.deploymentType()} +} + +func (cfg EdgeletInstallConfig) hostOS() string { + if cfg.HostOS != "" { + return cfg.HostOS + } + return "linux" +} + +func (cfg EdgeletInstallConfig) shareDir() string { + return EdgeletShareDir(cfg.hostOS()) +} + +func (cfg EdgeletInstallConfig) containerEngineURL() string { + engine := cfg.containerEngine() + if cfg.Runtime != nil { + return resolveContainerEngineURL(engine, &cfg.Runtime.Agent) + } + return resolveContainerEngineURL(engine, nil) +} + +func (cfg EdgeletInstallConfig) bootstrapEnv(localInstall bool) string { + parts := []string{ + fmt.Sprintf("EDGELET_INSTALL_MODE=%s", cfg.installModeEnv()), + fmt.Sprintf("CONTAINER_ENGINE=%s", cfg.containerEngine()), + fmt.Sprintf("DEPLOYMENT_TYPE=%s", cfg.deploymentType()), + fmt.Sprintf("EDGELET_VERSION=%s", cfg.version()), + fmt.Sprintf("EDGELET_CONTAINER_IMAGE=%s", cfg.containerImage()), + fmt.Sprintf("EDGELET_TZ=%s", cfg.timeZone()), + fmt.Sprintf("EDGELET_GITHUB_REPO=%s", util.GetEdgeletGitHubRepo()), + fmt.Sprintf("EDGELET_CONTAINER_ENGINE_URL=%s", cfg.containerEngineURL()), + } + if localInstall { + parts = append(parts, "LOCAL_INSTALL=1") + } + if localInstall && IsDesktopContainerDeploy(cfg) { + parts = append(parts, fmt.Sprintf("EDGELET_SCRIPT_STAGE_DIR=%s", EdgeletScriptStageDir)) + pathEnv := os.Getenv("PATH") + if pathEnv == "" { + pathEnv = "/usr/bin:/bin" + } + parts = append(parts, fmt.Sprintf("PATH=%s/bin:%s", EdgeletScriptStageDir, pathEnv)) + } + if IsDesktopContainerDeploy(cfg) { + if cmd, err := cfg.bootstrapConfigCommand(); err == nil && cmd != "" { + parts = append(parts, fmt.Sprintf("EDGELET_BOOTSTRAP_CONFIG_CMD=%s", shellQuoteArg(cmd))) + } + } + return strings.Join(parts, " ") +} + +func (cfg EdgeletInstallConfig) nativeInstallFlags() ([]string, error) { + flags := []string{ + fmt.Sprintf("--version=%s", cfg.version()), + fmt.Sprintf("--container-engine=%s", cfg.containerEngine()), + "--skip-config", + "--skip-start", + } + if cfg.Arch != "" && cfg.Arch != "auto" { + flags = append(flags, fmt.Sprintf("--arch=%s", cfg.Arch)) + } + if cfg.Airgap { + flags = append(flags, "--airgap") + if cfg.BinPath != "" { + flags = append(flags, fmt.Sprintf("--bin-path=%s", cfg.BinPath)) + } + } + return flags, nil +} + +func (cfg EdgeletInstallConfig) containerInstallFlags() []string { + return []string{ + fmt.Sprintf("--image=%s", cfg.containerImage()), + fmt.Sprintf("--engine=%s", cfg.containerEngine()), + fmt.Sprintf("--tz=%s", cfg.timeZone()), + } +} + +func (cfg EdgeletInstallConfig) installFlags() ([]string, error) { + if cfg.native() { + return cfg.nativeInstallFlags() + } + return cfg.containerInstallFlags(), nil +} + +func (cfg EdgeletInstallConfig) uninstallArgs(removeData bool) []string { + if !removeData { + return nil + } + return []string{"--remove-data"} +} + +// EdgeletProcedures extends AgentProcedures with edgelet bootstrap layers. +type EdgeletProcedures struct { + AgentProcedures + DetectInit Entrypoint + InstallContainer Entrypoint + InstallInitUnits Entrypoint + StartEdgelet Entrypoint + ConfigureContainer Entrypoint + WaitEdgeletReady Entrypoint + Bundled Entrypoint +} + +func edgeletScriptNames() []string { + names := []string{ + pkg.edgeletScriptPrereq, + pkg.edgeletScriptDetectInit, + pkg.edgeletScriptInstallDeps, + pkg.edgeletScriptConfigureContainerEngine, + pkg.edgeletScriptInstall, + pkg.edgeletScriptInstallContainer, + pkg.edgeletScriptInstallInitUnits, + pkg.edgeletScriptStartEdgelet, + pkg.edgeletScriptConfigureContainerEdgelet, + pkg.edgeletScriptWaitEdgeletReady, + pkg.edgeletScriptBundled, + pkg.edgeletScriptUninstall, + } + return append(names, pkg.edgeletLibScripts...) +} + +func addEdgeletAssetPrefix(file string) string { + return "edgelet/scripts/" + file +} + +func loadEdgeletScript(name string) (string, error) { + return util.GetStaticFile(addEdgeletAssetPrefix(name)) +} + +func loadDefaultEdgeletScripts() ([]string, []string, error) { + names := edgeletScriptNames() + contents := make([]string, 0, len(names)) + for _, name := range names { + content, err := loadEdgeletScript(name) + if err != nil { + return nil, nil, err + } + contents = append(contents, content) + } + return names, contents, nil +} + +func newDefaultEdgeletProcedures(dir string, cfg EdgeletInstallConfig) (EdgeletProcedures, error) { + installFlags, err := cfg.installFlags() + if err != nil { + return EdgeletProcedures{}, err + } + + installEntry := Entrypoint{ + Name: pkg.edgeletScriptInstall, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptInstall), + Args: installFlags, + } + if !cfg.native() { + installEntry = Entrypoint{ + Name: pkg.edgeletScriptInstallContainer, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptInstallContainer), + Args: installFlags, + } + } + + procs := EdgeletProcedures{ + AgentProcedures: AgentProcedures{ + check: Entrypoint{ + Name: pkg.edgeletScriptPrereq, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptPrereq), + }, + Deps: Entrypoint{ + Name: pkg.edgeletScriptInstallDeps, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptInstallDeps), + Args: cfg.depsArgs(), + }, + Install: installEntry, + Uninstall: Entrypoint{ + Name: pkg.edgeletScriptUninstall, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptUninstall), + }, + }, + DetectInit: Entrypoint{ + Name: pkg.edgeletScriptDetectInit, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptDetectInit), + }, + InstallContainer: Entrypoint{ + Name: pkg.edgeletScriptInstallContainer, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptInstallContainer), + Args: cfg.containerInstallFlags(), + }, + InstallInitUnits: Entrypoint{ + Name: pkg.edgeletScriptInstallInitUnits, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptInstallInitUnits), + }, + StartEdgelet: Entrypoint{ + Name: pkg.edgeletScriptStartEdgelet, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptStartEdgelet), + }, + ConfigureContainer: Entrypoint{ + Name: pkg.edgeletScriptConfigureContainerEdgelet, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptConfigureContainerEdgelet), + }, + WaitEdgeletReady: Entrypoint{ + Name: pkg.edgeletScriptWaitEdgeletReady, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptWaitEdgeletReady), + }, + Bundled: Entrypoint{ + Name: pkg.edgeletScriptBundled, + destPath: util.JoinAgentPath(dir, pkg.edgeletScriptBundled), + }, + } + + names, contents, err := loadDefaultEdgeletScripts() + if err != nil { + return EdgeletProcedures{}, err + } + procs.scriptNames = names + procs.scriptContents = contents + return procs, nil +} + +func (procs *EdgeletProcedures) setInstallArgs(cfg EdgeletInstallConfig) error { + flags, err := cfg.installFlags() + if err != nil { + return err + } + procs.refreshInstallEntry(stageDirFromEntrypoint(procs.Install.destPath), cfg) + procs.Install.Args = flags + procs.InstallContainer.Args = cfg.containerInstallFlags() + return nil +} + +func stageDirFromEntrypoint(destPath string) string { + if destPath == "" { + return EdgeletScriptStageDir + } + if idx := strings.LastIndex(destPath, "/"); idx > 0 { + return destPath[:idx] + } + return EdgeletScriptStageDir +} + +func (procs *EdgeletProcedures) refreshInstallEntry(stageDir string, cfg EdgeletInstallConfig) { + if cfg.native() { + procs.Install.Name = pkg.edgeletScriptInstall + procs.Install.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptInstall) + return + } + procs.Install.Name = pkg.edgeletScriptInstallContainer + procs.Install.destPath = util.JoinAgentPath(stageDir, pkg.edgeletScriptInstallContainer) +} + +func (procs *EdgeletProcedures) setUninstallArgs(cfg EdgeletInstallConfig, removeData bool) { + procs.Uninstall.Args = cfg.uninstallArgs(removeData) +} + +func (procs *EdgeletProcedures) setDepsArgs(cfg EdgeletInstallConfig) { + procs.Deps.Args = cfg.depsArgs() +} + +// wrapBootstrapCommand prefixes bootstrap env vars. When useSudo is true, env is passed via +// "sudo env ..." so macOS/Linux sudo does not strip CONTAINER_ENGINE and related vars. +func wrapBootstrapCommand(cmd, env string, useSudo bool) string { + if env == "" { + return cmd + } + if useSudo && strings.HasPrefix(cmd, "sudo ") { + return "sudo env " + env + " " + strings.TrimPrefix(cmd, "sudo ") + } + return env + " " + cmd +} + +func (procs *EdgeletProcedures) preInstallCommands(name string, cfg EdgeletInstallConfig, useSudo bool) []command { + prefix := "" + if useSudo { + prefix = "sudo " + } + env := cfg.bootstrapEnv(!useSudo) + withEnv := func(cmd string) string { + return wrapBootstrapCommand(cmd, env, useSudo) + } + return []command{ + {cmd: withEnv(procs.check.getCommand()), msg: "Checking prerequisites on " + name}, + {cmd: withEnv(procs.DetectInit.getCommand()), msg: "Detecting OS/init on " + name}, + {cmd: withEnv(procs.Deps.getCommand()), msg: "Installing dependencies on " + name}, + {cmd: withEnv(prefix + procs.Install.getCommand()), msg: "Installing edgelet on " + name}, + } +} + +func (procs *EdgeletProcedures) postInstallCommands(name string, cfg EdgeletInstallConfig, useSudo bool) []command { + prefix := "" + if useSudo { + prefix = "sudo " + } + env := cfg.bootstrapEnv(!useSudo) + withEnv := func(cmd string) string { + return wrapBootstrapCommand(cmd, env, useSudo) + } + cmds := []command{ + {cmd: withEnv(prefix + procs.InstallInitUnits.getCommand()), msg: "Installing edgelet init units on " + name}, + {cmd: withEnv(prefix + procs.StartEdgelet.getCommand()), msg: "Starting edgelet on " + name}, + {cmd: withEnv(prefix + procs.ConfigureContainer.getCommand()), msg: "Configuring edgelet container on " + name}, + {cmd: withEnv(prefix + procs.WaitEdgeletReady.getCommand()), msg: "Waiting for edgelet on " + name}, + {cmd: withEnv(prefix + procs.Bundled.getCommand()), msg: "Publishing edgelet scripts on " + name}, + } + return cmds +} + +func isEdgeletNotProvisionedError(err error) bool { + if err == nil { + return false + } + return strings.Contains(strings.ToLower(err.Error()), "not provisioned") +} + +func isEdgeletControlPlaneRemovedError(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "not found") || + strings.Contains(msg, "no control plane") || + strings.Contains(msg, "already removed") +} diff --git a/pkg/iofog/install/k8s.go b/pkg/iofog/install/k8s.go index 3c907542b..3748053b4 100644 --- a/pkg/iofog/install/k8s.go +++ b/pkg/iofog/install/k8s.go @@ -1,35 +1,18 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package install import ( - "bytes" "context" "fmt" - "io" "net/url" "reflect" "strings" "time" ioclient "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - iofogv3 "github.com/eclipse-iofog/iofog-operator/v3/apis" cpv3 "github.com/eclipse-iofog/iofog-operator/v3/apis/controlplanes/v3" + opk8s "github.com/eclipse-iofog/iofog-operator/v3/pkg/k8s" "github.com/eclipse-iofog/iofogctl/pkg/util" corev1 "k8s.io/api/core/v1" - networkingv1 "k8s.io/api/networking/v1" - extsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" extsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -41,11 +24,6 @@ import ( opclient "sigs.k8s.io/controller-runtime/pkg/client" ) -// ECNname -const ( - cpInstanceName = "iofog" -) - // Kubernetes struct to manage state of deployment on Kubernetes cluster type Kubernetes struct { config *restclient.Config @@ -57,9 +35,8 @@ type Kubernetes struct { services cpv3.Services images cpv3.Images ingresses cpv3.Ingresses - httpsEnabled *bool // Store HTTPS configuration - isViewerDns *bool // Store isViewerDns configuration - // router cpv3.Router + httpsEnabled *bool + cpSpec cpv3.ControlPlaneSpec } // NewKubernetes constructs an object to manage cluster @@ -132,14 +109,12 @@ func (k8s *Kubernetes) SetHttpsEnabled(enabled *bool) { k8s.httpsEnabled = enabled } -func (k8s *Kubernetes) SetIsViewerDns(enabled *bool) { - k8s.isViewerDns = enabled -} +const controlPlaneReadyTimeout = 600 * time.Second +const controlPlanePollInterval = 3 * time.Second func (k8s *Kubernetes) enableCustomResources() error { ctx := context.Background() - // Control Plane and App - for _, crd := range []*extsv1.CustomResourceDefinition{iofogv3.NewControlPlaneCustomResource(), iofogv3.NewAppCustomResource()} { + for _, crd := range controlPlaneCRDsToInstall() { // Try create new if _, err := k8s.extsClientset.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, crd, metav1.CreateOptions{}); err != nil { if !k8serrors.IsAlreadyExists(err) { @@ -154,7 +129,7 @@ func (k8s *Kubernetes) enableCustomResources() error { // Always update the CRD if: // 1. The CRD is not supported (major version mismatch) // 2. The versions array is different (new features/types added) - shouldUpdate := !iofogv3.IsSupportedCustomResource(existingCRD) || + shouldUpdate := !isSupportedControlPlaneCRD(existingCRD, crd) || !reflect.DeepEqual(existingCRD.Spec.Versions, crd.Spec.Versions) if shouldUpdate { @@ -183,7 +158,7 @@ func (k8s *Kubernetes) enableCustomResources() error { } func (k8s *Kubernetes) enableOperatorClient() (err error) { - scheme := iofogv3.InitClientScheme() + scheme := initOperatorClientScheme() k8s.opClient, err = opclient.New(k8s.config, opclient.Options{Scheme: scheme}) if err != nil { return err @@ -191,8 +166,8 @@ func (k8s *Kubernetes) enableOperatorClient() (err error) { return nil } -// CreateController on cluster -func (k8s *Kubernetes) CreateControlPlane(conf *K8SControllerConfig) (endpoint string, err error) { +// CreateControlPlane applies or updates the operator ControlPlane CR in the cluster. +func (k8s *Kubernetes) CreateControlPlane(desired cpv3.ControlPlane) (endpoint string, err error) { // Create namespace if required Verbose("Creating namespace " + k8s.ns) ns := &corev1.Namespace{ @@ -214,8 +189,9 @@ func (k8s *Kubernetes) CreateControlPlane(conf *K8SControllerConfig) (endpoint s // Check if Control Plane exists Verbose("Finding existing Control Plane") + crName := util.GetCliCpCrName() cpKey := opclient.ObjectKey{ - Name: cpInstanceName, + Name: crName, Namespace: k8s.ns, } var cp cpv3.ControlPlane @@ -228,40 +204,17 @@ func (k8s *Kubernetes) CreateControlPlane(conf *K8SControllerConfig) (endpoint s found = false cp = cpv3.ControlPlane{ ObjectMeta: metav1.ObjectMeta{ - Name: cpInstanceName, + Name: crName, Namespace: k8s.ns, }, } } - // Set specification - cp.Spec.Replicas.Controller = conf.Replicas - if conf.ReplicasNats >= 2 { - cp.Spec.Replicas.Nats = conf.ReplicasNats - } - cp.Spec.Database = cpv3.Database(conf.Database) - cp.Spec.Auth = cpv3.Auth(conf.Auth) - cp.Spec.Events = cpv3.Events(conf.Events) - // cp.Spec.User = cpv3.User(conf.User) - cp.Spec.Services = k8s.services - cp.Spec.Ingresses = k8s.ingresses - cp.Spec.Images = k8s.images - if conf.Nats != nil { - cp.Spec.Nats = conf.Nats - } - if conf.Vault != nil { - cp.Spec.Vault = conf.Vault - } - // cp.Spec.Router = k8s.router - cp.Spec.Controller.EcnViewerPort = conf.EcnViewerPort - cp.Spec.Controller.EcnViewerURL = conf.EcnViewerURL - cp.Spec.Controller.LogLevel = conf.LogLevel - cp.Spec.Controller.PidBaseDir = conf.PidBaseDir - cp.Spec.Controller.Https = conf.Https - cp.Spec.Controller.SecretName = conf.SecretName + cp.Spec = desired.Spec + k8s.cpSpec = desired.Spec // Store HTTPS configuration for endpoint generation - k8s.SetHttpsEnabled(conf.Https) + k8s.SetHttpsEnabled(desired.Spec.Controller.Https) // Create or update Control Plane if found { @@ -277,117 +230,39 @@ func (k8s *Kubernetes) CreateControlPlane(conf *K8SControllerConfig) (endpoint s } } - // Get endpoint of deployed Controller - endpoint, err = k8s.GetControllerEndpoint() - if err != nil { + if err = k8s.waitControlPlaneReady(context.Background(), controlPlaneReadyTimeout); err != nil { return } - // Wait for Default Router to be registered by Port Manager - errCh := make(chan error, 1) - go k8s.monitorOperator(errCh) - select { - case err = <-errCh: - case <-time.After(600 * time.Second): - err = util.NewInternalError("Failed to wait for Default Router registration") - } - + endpoint, err = k8s.GetControllerEndpoint() return endpoint, err } -func (k8s *Kubernetes) getReadyPod() (readyPod *corev1.Pod, err error) { - // Check operator logs - pods, err := k8s.clientset.CoreV1().Pods(k8s.ns).List(context.Background(), metav1.ListOptions{ - LabelSelector: "name=iofog-operator", // TODO: Decouple this - }) - if err != nil { - return - } - if len(pods.Items) == 0 { - err = util.NewInternalError("Could not find any Operator Pods ") - return - } - // Find ready Pod - var pod *corev1.Pod - for podIdx := range pods.Items { - for _, condition := range pods.Items[podIdx].Status.Conditions { - if condition.Type == corev1.PodReady { - if condition.Status == corev1.ConditionTrue { - pod = &pods.Items[podIdx] - break - } - } - } - if pod != nil { - break - } +func (k8s *Kubernetes) waitControlPlaneReady(ctx context.Context, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + cpKey := opclient.ObjectKey{ + Name: util.GetCliCpCrName(), + Namespace: k8s.ns, } - return pod, err -} + ticker := time.NewTicker(controlPlanePollInterval) + defer ticker.Stop() -// Watch Operator logs -// Report error from Operator if found in logs -// Operator Pods are deleted and created when Control Plane redeployed -func (k8s *Kubernetes) monitorOperator(errCh chan error) { - errSuffix := "while awaiting finalization of Control Plane" for { - time.Sleep(2 * time.Second) - pod, err := k8s.getReadyPod() - if err != nil { - errCh <- fmt.Errorf("%s %s", err.Error(), errSuffix) - return - } - // Could not find ready Operator Pod - if pod == nil { - continue - } - // Get the logs of ready Pod - req := k8s.clientset.CoreV1().Pods(k8s.ns).GetLogs(pod.Name, &corev1.PodLogOptions{}) - podLogs, err := req.Stream(context.Background()) - if err != nil { - errCh <- util.NewInternalError("Error opening Operator Pod log stream " + errSuffix) - return - } - defer podLogs.Close() - buf := new(bytes.Buffer) - if _, err = io.Copy(buf, podLogs); err != nil { - errCh <- util.NewInternalError("Error reading Operator Pod log stream " + errSuffix) - return - } - - // Check controlplane resource status var cp cpv3.ControlPlane - if err = k8s.opClient.Get(context.Background(), opclient.ObjectKey{ - Name: cpInstanceName, - Namespace: k8s.ns, - }, &cp); err != nil { - errCh <- util.NewInternalError("Error reading Control Plane resource " + errSuffix) - return + if err := k8s.opClient.Get(ctx, cpKey, &cp); err != nil { + return util.NewInternalError("Error reading Control Plane resource: " + err.Error()) } - if cp.IsReady() { - errCh <- nil - return + return nil + } + if time.Now().After(deadline) { + return util.NewInternalError("Timed out waiting for Control Plane to become ready") + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: } - - // podLogsStr := buf.String() - // Allow errors, need to fix iofog-operator to not have errors anymore - // errDelim := `ERROR` // TODO: Decouple iofogctl-operator err string - // if strings.Contains(podLogsStr, errDelim) { - // msg := "" - // logLines := strings.Split(podLogsStr, "\n") - // for _, line := range logLines { - // // Error line pertains to this NS? - // if strings.Contains(line, errDelim) && strings.Contains(line, fmt.Sprintf(`"namespace": "%s"`, k8s.ns)) { - // msg = fmt.Sprintf("%s\n%s", msg, line) - // errCh <- util.NewInternalError("Operator failed to reconcile Control Plane " + msg) - // return - // } - // } - // Error pertains to another Control Plane - // } - - // Continue loop, wait for Router registration or error... } } @@ -472,7 +347,7 @@ func (k8s *Kubernetes) createOperator() (err error) { return nil } -func (k8s *Kubernetes) DeleteControlPlane() error { +func (k8s *Kubernetes) DeleteControlPlane(deleteNamespace bool) error { // Prepare Control Plane client if err := k8s.enableOperatorClient(); err != nil { return err @@ -481,7 +356,7 @@ func (k8s *Kubernetes) DeleteControlPlane() error { // Delete Control Plane cp := &cpv3.ControlPlane{ ObjectMeta: metav1.ObjectMeta{ - Name: cpInstanceName, + Name: util.GetCliCpCrName(), Namespace: k8s.ns, }, } @@ -496,8 +371,7 @@ func (k8s *Kubernetes) DeleteControlPlane() error { return err } - // Delete Namespace - if k8s.ns != "default" { + if deleteNamespace && k8s.ns != "default" { if err := k8s.clientset.CoreV1().Namespaces().Delete(context.Background(), k8s.ns, metav1.DeleteOptions{}); err != nil { if !k8serrors.IsNotFound(err) { return err @@ -509,103 +383,117 @@ func (k8s *Kubernetes) DeleteControlPlane() error { } func (k8s *Kubernetes) waitForService(name string, targetPort int32) (addr string, nodePort int32, err error) { - // Get watch handler to observe changes to services - watch, err := k8s.clientset.CoreV1().Services(k8s.ns).Watch(context.Background(), metav1.ListOptions{}) + ctx, cancel := context.WithTimeout(context.Background(), controlPlaneReadyTimeout) + defer cancel() + + svc, err := k8s.waitForServiceExists(ctx, name) if err != nil { - return + return "", 0, err } - defer watch.Stop() - // Wait for Services to have addresses allocated - for event := range watch.ResultChan() { - svc, ok := event.Object.(*corev1.Service) - if !ok { - err = util.NewInternalError("Failed to wait for services in namespace: " + k8s.ns) - return - } - - // Ignore irrelevant service events - if svc.Name != name { - continue + switch svc.Spec.Type { + case corev1.ServiceTypeLoadBalancer: + getter := opk8s.CoreV1ServiceGetter{Kube: k8s.clientset} + remaining := time.Until(deadlineFromContext(ctx)) + addr, err = opk8s.WaitLoadBalancerAddress(ctx, getter, k8s.ns, name, remaining) + if err != nil { + return "", 0, err } + return addr, targetPort, nil - switch svc.Spec.Type { - case corev1.ServiceTypeLoadBalancer: - // Load balancer must be ready - if len(svc.Status.LoadBalancer.Ingress) == 0 { - continue - } - addr, nodePort = k8s.handleLoadBalancer(svc, targetPort) - if addr == "" { - continue - } - return - - case corev1.ServiceTypeNodePort: - addr, err = k8s.getNodePortAddress(name) - if err != nil { - util.PrintNotify("Could not get an external IP address of any Kubernetes nodes for NodePort service " + name + "\nTrying to reach the cluster IP of the service") - addr, err = k8s.getClusterIPAddress(name) - if err != nil { - return - } - } - nodePort, err = k8s.getPort(svc, name, targetPort) - return - case corev1.ServiceTypeClusterIP: - // Ingress must be ready for ClusterIP service type - addr, err = k8s.waitForIngress("iofog-controller") + case corev1.ServiceTypeNodePort: + addr, err = k8s.getNodePortAddress(name) + if err != nil { + util.PrintNotify("Could not get an external IP address of any Kubernetes nodes for NodePort service " + name + "\nTrying to reach the cluster IP of the service") + addr, err = k8s.getClusterIPAddress(name) if err != nil { - util.PrintNotify("Failed to handle Ingress for ClusterIP service") - continue - } - if addr == "" { - continue + return "", 0, err } - nodePort = targetPort - return - default: - err = util.NewError("Found Service was not of supported type") - return } + nodePort, err = k8s.getPort(svc, name, targetPort) + return addr, nodePort, err + + case corev1.ServiceTypeClusterIP: + addr, err = k8s.waitForIngress(ctx, controller) + if err != nil { + return "", 0, err + } + return addr, targetPort, nil + + default: + return "", 0, util.NewError("Found Service was not of supported type") } - err = util.NewError("Did not receive any events from Kuberenetes API Server") - return addr, nodePort, err } -func (k8s *Kubernetes) waitForIngress(name string) (addr string, err error) { - // Create a watch to observe changes to Ingress resources - watcher, err := k8s.clientset.NetworkingV1().Ingresses(k8s.ns).Watch(context.Background(), metav1.ListOptions{}) - if err != nil { - err = util.NewError("Failed to create watch for Ingress: " + err.Error()) - return +func deadlineFromContext(ctx context.Context) time.Time { + if d, ok := ctx.Deadline(); ok { + return d } - defer watcher.Stop() + return time.Now().Add(controlPlaneReadyTimeout) +} - // Process events from the watch - for event := range watcher.ResultChan() { - ingress, ok := event.Object.(*networkingv1.Ingress) - if !ok { - err = util.NewInternalError("Failed to parse Ingress event") - return +func (k8s *Kubernetes) waitForServiceExists(ctx context.Context, name string) (*corev1.Service, error) { + ticker := time.NewTicker(controlPlanePollInterval) + defer ticker.Stop() + for { + svc, err := k8s.clientset.CoreV1().Services(k8s.ns).Get(ctx, name, metav1.GetOptions{}) + if err == nil { + return svc, nil + } + if !k8serrors.IsNotFound(err) { + return nil, err } + select { + case <-ctx.Done(): + return nil, util.NewInternalError("Timed out waiting for service " + name) + case <-ticker.C: + } + } +} - // Check if the Ingress resource matches the name we're waiting for - if ingress.Name == name { - // Check if Ingress has rules - if len(ingress.Spec.Rules) > 0 { - host := ingress.Spec.Rules[0].Host - addr = "https://" + host - return - } +func (k8s *Kubernetes) waitForIngress(ctx context.Context, name string) (addr string, err error) { + scheme := "http" + if k8s.ingressUsesHTTPS() { + scheme = "https" + } - // Ingress found but has no rules, continue waiting - util.PrintNotify("Ingress resource found but no rules present, continuing to watch...") + ticker := time.NewTicker(controlPlanePollInterval) + defer ticker.Stop() + for { + ingress, err := k8s.clientset.NetworkingV1().Ingresses(k8s.ns).Get(ctx, name, metav1.GetOptions{}) + if err == nil && len(ingress.Spec.Rules) > 0 && ingress.Spec.Rules[0].Host != "" { + return scheme + "://" + ingress.Spec.Rules[0].Host, nil + } + if err != nil && !k8serrors.IsNotFound(err) { + return "", err + } + select { + case <-ctx.Done(): + return "", util.NewInternalError("Timed out waiting for Ingress " + name) + case <-ticker.C: } } +} - err = util.NewError("Did not receive any valid Ingress events") - return +func (k8s *Kubernetes) ingressUsesHTTPS() bool { + if k8s.httpsEnabled != nil && *k8s.httpsEnabled { + return true + } + if k8s.cpSpec.Ingresses.Controller.SecretName != "" { + return true + } + return false +} + +func (k8s *Kubernetes) getCRPublicURL(ctx context.Context) string { + var cp cpv3.ControlPlane + if err := k8s.opClient.Get(ctx, opclient.ObjectKey{ + Name: util.GetCliCpCrName(), + Namespace: k8s.ns, + }, &cp); err != nil { + return "" + } + return cp.Spec.Controller.PublicUrl } func (k8s *Kubernetes) getPort(svc *corev1.Service, name string, targetPort int32) (nodePort int32, err error) { @@ -672,20 +560,6 @@ func (k8s *Kubernetes) getNodePortAddress(name string) (addr string, err error) return } -func (k8s *Kubernetes) handleLoadBalancer(svc *corev1.Service, targetPort int32) (addr string, nodePort int32) { - nodePort = targetPort - // TODO: error if Ingress len == 0 - ip := svc.Status.LoadBalancer.Ingress[0].IP - host := svc.Status.LoadBalancer.Ingress[0].Hostname - if ip != "" { - addr = ip - } - if host != "" { - addr = host - } - return -} - func (k8s *Kubernetes) SetControllerService(svcType, address string, annotations map[string]string, externalTrafficPolicy string) { if svcType != "" { k8s.services.Controller.Type = svcType @@ -782,7 +656,7 @@ func (k8s *Kubernetes) ExistsInNamespace(namespace string) error { return util.NewError("Could not find Controller Service in Kubernetes namespace " + namespace) } -func (k8s *Kubernetes) formatEndpoint(endpoint string, port int32, isViewerDns ...bool) (*url.URL, error) { +func (k8s *Kubernetes) formatEndpoint(endpoint string, port int32) (*url.URL, error) { // Ensure protocol if !strings.Contains(endpoint, "://") { // Check if HTTPS should be used @@ -796,11 +670,8 @@ func (k8s *Kubernetes) formatEndpoint(endpoint string, port int32, isViewerDns . if err != nil { return nil, err } - // Ensure port is added if not present - // if !strings.Contains(URL.Host, ":") { - // Ensure port when scheme is not HTTPS OR when isViewerDns is false - if !strings.Contains(URL.Host, ":") && ((URL.Scheme != "https") || (len(isViewerDns) > 0 && !isViewerDns[0])) { - + // Ensure port on non-HTTPS endpoints when omitted. + if !strings.Contains(URL.Host, ":") && URL.Scheme != "https" { URL.Host += fmt.Sprintf(":%d", port) } return URL, nil @@ -810,24 +681,25 @@ func (k8s *Kubernetes) formatEndpoint(endpoint string, port int32, isViewerDns . func (k8s *Kubernetes) GetControllerEndpoint() (endpoint string, err error) { ip, port, err := k8s.waitForService(controller, ioclient.ControllerPort) if err != nil { + if publicURL := k8s.getCRPublicURL(context.Background()); publicURL != "" { + return publicURL, nil + } return "", err } - isViewerDns := false - if k8s.isViewerDns != nil && *k8s.isViewerDns { - isViewerDns = true + if ip == "" { + if publicURL := k8s.getCRPublicURL(context.Background()); publicURL != "" { + return publicURL, nil + } + return "", util.NewInternalError("Could not resolve Controller endpoint") } - formattedURL, err := k8s.formatEndpoint(ip, port, isViewerDns) + + formattedURL, err := k8s.formatEndpoint(ip, port) if err != nil { return "", err } endpoint = formattedURL.String() - // Check if HTTPS is enabled - useHTTPS := false - if k8s.httpsEnabled != nil && *k8s.httpsEnabled { - useHTTPS = true - } - + useHTTPS := k8s.httpsEnabled != nil && *k8s.httpsEnabled return util.GetControllerEndpoint(endpoint, useHTTPS) } @@ -838,9 +710,10 @@ func (k8s *Kubernetes) GetControllerPods() (podNames []Pod, err error) { if err != nil { return } - // Find Controller pods + // Find Controller pods (label key follows build flavor: e.g. datasance.com/component or iofog.org/component) + componentLabel := util.GetCliCrdGroup() + "/component" for idx := range pods.Items { - if pods.Items[idx].Labels["iofog.org/component"] == controller { + if controllerPodLabelsMatch(pods.Items[idx].Labels, componentLabel) { podNames = append(podNames, Pod{ Name: pods.Items[idx].Name, Status: string(pods.Items[idx].Status.Phase), @@ -849,3 +722,7 @@ func (k8s *Kubernetes) GetControllerPods() (podNames []Pod, err error) { } return } + +func controllerPodLabelsMatch(labels map[string]string, componentLabel string) bool { + return labels[componentLabel] == controller +} diff --git a/pkg/iofog/install/k8s_controller_pods_test.go b/pkg/iofog/install/k8s_controller_pods_test.go new file mode 100644 index 000000000..ee29f4320 --- /dev/null +++ b/pkg/iofog/install/k8s_controller_pods_test.go @@ -0,0 +1,57 @@ +package install + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestControllerPodLabelsMatch(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + labels map[string]string + componentLabel string + wantMatch bool + }{ + { + name: "datasance operator controller pod", + componentLabel: "datasance.com/component", + labels: map[string]string{ + "datasance.com/component": "controller", + "app.kubernetes.io/component": "controller", + }, + wantMatch: true, + }, + { + name: "iofog eclipse controller pod", + componentLabel: "iofog.org/component", + labels: map[string]string{ + "iofog.org/component": "controller", + }, + wantMatch: true, + }, + { + name: "wrong component value", + componentLabel: "datasance.com/component", + labels: map[string]string{ + "datasance.com/component": "router", + }, + }, + { + name: "legacy key only", + componentLabel: "datasance.com/component", + labels: map[string]string{ + "iofog.org/component": "controller", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.wantMatch, controllerPodLabelsMatch(tt.labels, tt.componentLabel)) + }) + } +} diff --git a/pkg/iofog/install/k8s_crd.go b/pkg/iofog/install/k8s_crd.go new file mode 100644 index 000000000..c5d1c5a52 --- /dev/null +++ b/pkg/iofog/install/k8s_crd.go @@ -0,0 +1,83 @@ +package install + +import ( + "fmt" + + "github.com/eclipse-iofog/iofogctl/pkg/util" + extsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func controlPlaneCRDName(group string) string { + return fmt.Sprintf("controlplanes.%s", group) +} + +func newControlPlaneCRD(group string) *extsv1.CustomResourceDefinition { + apiVersions := []string{"v3"} + versions := make([]extsv1.CustomResourceDefinitionVersion, len(apiVersions)) + preserveUnknownFields := true + + for i, version := range apiVersions { + versions[i].Name = version + versions[i].Served = true + + if i == 0 { + versions[i].Storage = true + } + + versions[i].Schema = &extsv1.CustomResourceValidation{ + OpenAPIV3Schema: &extsv1.JSONSchemaProps{ + Properties: map[string]extsv1.JSONSchemaProps{}, + XPreserveUnknownFields: &preserveUnknownFields, + Type: "object", + }, + } + versions[i].Subresources = &extsv1.CustomResourceSubresources{ + Status: &extsv1.CustomResourceSubresourceStatus{}, + } + } + + return &extsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: controlPlaneCRDName(group), + }, + Spec: extsv1.CustomResourceDefinitionSpec{ + Group: group, + Names: extsv1.CustomResourceDefinitionNames{ + Kind: "ControlPlane", + ListKind: "ControlPlaneList", + Plural: "controlplanes", + Singular: "controlplane", + }, + Scope: extsv1.NamespaceScoped, + Versions: versions, + }, + } +} + +func sameCRDVersionsSupported(left, right *extsv1.CustomResourceDefinition) bool { + for _, leftVersion := range left.Spec.Versions { + matched := false + for _, rightVersion := range right.Spec.Versions { + if leftVersion.Name == rightVersion.Name { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + +func isSupportedControlPlaneCRD(existing, expected *extsv1.CustomResourceDefinition) bool { + if existing.Name != expected.Name { + return false + } + return sameCRDVersionsSupported(expected, existing) +} + +func controlPlaneCRDsToInstall() []*extsv1.CustomResourceDefinition { + return []*extsv1.CustomResourceDefinition{newControlPlaneCRD(util.GetCliCrdGroup())} +} diff --git a/pkg/iofog/install/k8s_microservices.go b/pkg/iofog/install/k8s_microservices.go index d35e22324..bf3bd35dd 100644 --- a/pkg/iofog/install/k8s_microservices.go +++ b/pkg/iofog/install/k8s_microservices.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package install import ( @@ -47,98 +34,10 @@ type container struct { func newOperatorMicroservice() *microservice { return µservice{ - name: "iofog-operator", - ports: []int32{60000}, - replicas: 1, - rbacRules: []rbacv1.PolicyRule{ - { - APIGroups: []string{ - "rbac.authorization.k8s.io", - }, - Resources: []string{ - "roles", - "rolebindings", - }, - Verbs: []string{ - "*", - }, - }, - { - APIGroups: []string{ - "networking.k8s.io", - }, - Resources: []string{ - "ingresses", - "ingresses/status", - }, - Verbs: []string{ - "*", - }, - }, - { - APIGroups: []string{ - "iofog.org", - }, - Resources: []string{ - "apps", - "applications", - "applications/status", - "controlplanes", - "apps/status", - "controlplanes/status", - "apps/finalizers", - "applications/finalizers", - "controlplanes/finalizers", - }, - Verbs: []string{ - "list", - "get", - "watch", - "update", - }, - }, - { - APIGroups: []string{ - "apps", - }, - Resources: []string{ - "deployments", - "statefulsets", - }, - Verbs: []string{ - "*", - }, - }, - { - APIGroups: []string{ - "coordination.k8s.io", - }, - Resources: []string{ - "leases", - }, - Verbs: []string{ - "*", - }, - }, - { - APIGroups: []string{ - "", - }, - Resources: []string{ - "pods", - "configmaps", - "configmaps/status", - "events", - "serviceaccounts", - "services", - "persistentvolumeclaims", - "secrets", - }, - Verbs: []string{ - "*", - }, - }, - }, + name: "iofog-operator", + ports: []int32{60000}, + replicas: 1, + rbacRules: newOperatorRBACRules(), containers: []container{ { name: "iofog-operator", @@ -182,3 +81,53 @@ func newOperatorMicroservice() *microservice { }, } } + +func newOperatorRBACRules() []rbacv1.PolicyRule { + group := util.GetCliCrdGroup() + return []rbacv1.PolicyRule{ + { + APIGroups: []string{"coordination.k8s.io"}, + Resources: []string{"leases"}, + Verbs: []string{"*"}, + }, + { + APIGroups: []string{"rbac.authorization.k8s.io"}, + Resources: []string{"roles", "rolebindings"}, + Verbs: []string{"*"}, + }, + { + APIGroups: []string{"networking.k8s.io"}, + Resources: []string{"ingresses", "ingresses/status"}, + Verbs: []string{"*"}, + }, + { + APIGroups: []string{group}, + Resources: []string{"controlplanes"}, + Verbs: []string{"create", "delete", "get", "list", "patch", "update", "watch"}, + }, + { + APIGroups: []string{group}, + Resources: []string{"controlplanes/status", "controlplanes/finalizers"}, + Verbs: []string{"get", "patch", "update"}, + }, + { + APIGroups: []string{"apps"}, + Resources: []string{"deployments", "statefulsets"}, + Verbs: []string{"*"}, + }, + { + APIGroups: []string{""}, + Resources: []string{ + "pods", + "configmaps", + "configmaps/status", + "events", + "serviceaccounts", + "services", + "persistentvolumeclaims", + "secrets", + }, + Verbs: []string{"*"}, + }, + } +} diff --git a/pkg/iofog/install/k8s_operator_test.go b/pkg/iofog/install/k8s_operator_test.go new file mode 100644 index 000000000..c84a5ba28 --- /dev/null +++ b/pkg/iofog/install/k8s_operator_test.go @@ -0,0 +1,74 @@ +package install + +import ( + "testing" + + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" +) + +func TestControlPlaneCRDsToInstallSingleCRD(t *testing.T) { + crds := controlPlaneCRDsToInstall() + require.Len(t, crds, 1) + require.Equal(t, "controlplanes."+util.GetCliCrdGroup(), crds[0].Name) + require.Equal(t, util.GetCliCrdGroup(), crds[0].Spec.Group) +} + +func TestOperatorRBACUsesFlavorGroupNotApplications(t *testing.T) { + rules := newOperatorRBACRules() + group := util.GetCliCrdGroup() + + var hasControlPlaneRule bool + for _, rule := range rules { + for _, resource := range rule.Resources { + require.NotContains(t, resource, "application") + } + for _, apiGroup := range rule.APIGroups { + if apiGroup != group { + continue + } + for _, resource := range rule.Resources { + if resource != "controlplanes" { + continue + } + hasControlPlaneRule = true + require.ElementsMatch(t, + []string{"create", "delete", "get", "list", "patch", "update", "watch"}, + rule.Verbs, + ) + } + } + } + require.True(t, hasControlPlaneRule) +} + +func TestSetOperatorImageDefaultsFromLdflags(t *testing.T) { + k8s := &Kubernetes{operator: newOperatorMicroservice()} + k8s.SetOperatorImage("") + require.Equal(t, util.GetOperatorImage(), k8s.operator.containers[0].image) +} + +func TestOperatorDeploymentWatchNamespace(t *testing.T) { + ms := newOperatorMicroservice() + var watchEnv *corev1.EnvVar + for i := range ms.containers[0].env { + if ms.containers[0].env[i].Name == "WATCH_NAMESPACE" { + watchEnv = &ms.containers[0].env[i] + break + } + } + require.NotNil(t, watchEnv) + require.NotNil(t, watchEnv.ValueFrom) + require.NotNil(t, watchEnv.ValueFrom.FieldRef) + require.Equal(t, "metadata.namespace", watchEnv.ValueFrom.FieldRef.FieldPath) +} + +func TestIsSupportedControlPlaneCRD(t *testing.T) { + expected := newControlPlaneCRD(util.GetCliCrdGroup()) + existing := expected.DeepCopy() + require.True(t, isSupportedControlPlaneCRD(existing, expected)) + + wrongGroup := newControlPlaneCRD("example.com") + require.False(t, isSupportedControlPlaneCRD(wrongGroup, expected)) +} diff --git a/pkg/iofog/install/k8s_scheme.go b/pkg/iofog/install/k8s_scheme.go new file mode 100644 index 000000000..11946a009 --- /dev/null +++ b/pkg/iofog/install/k8s_scheme.go @@ -0,0 +1,22 @@ +package install + +import ( + cpv3 "github.com/eclipse-iofog/iofog-operator/v3/apis/controlplanes/v3" + "github.com/eclipse-iofog/iofogctl/pkg/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" +) + +func initOperatorClientScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + gv := schema.GroupVersion{Group: util.GetCliCrdGroup(), Version: "v3"} + scheme.AddKnownTypes(gv, &cpv3.ControlPlane{}, &cpv3.ControlPlaneList{}) + metav1.AddToGroupVersion(scheme, gv) + + return scheme +} diff --git a/pkg/iofog/install/k8s_util.go b/pkg/iofog/install/k8s_util.go index 4e8c1e751..da888af4b 100644 --- a/pkg/iofog/install/k8s_util.go +++ b/pkg/iofog/install/k8s_util.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package install import ( @@ -19,15 +6,18 @@ import ( rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/eclipse-iofog/iofogctl/pkg/util" ) -// operatorDeploymentLabels are applied to the iofog-operator Deployment and Pod template. -var operatorDeploymentLabels = map[string]string{ - "app.kubernetes.io/name": "iofog", - "app.kubernetes.io/instance": "iofog", - "app.kubernetes.io/component": "iofog-operator", - "app.kubernetes.io/managed-by": "iofogctl", - "iofog.org/component": "iofog-operator", +func operatorDeploymentLabels() map[string]string { + return map[string]string{ + "app.kubernetes.io/name": "iofog", + "app.kubernetes.io/instance": "iofog", + "app.kubernetes.io/component": "iofog-operator", + "app.kubernetes.io/managed-by": util.GetCliBinaryName(), + "iofog.org/component": "iofog-operator", + } } func newDeployment(namespace string, ms *microservice) *appsv1.Deployment { @@ -43,7 +33,7 @@ func newDeployment(namespace string, ms *microservice) *appsv1.Deployment { depLabels := map[string]string{"name": ms.name} podLabels := map[string]string{"name": ms.name} if ms.name == "iofog-operator" { - for k, v := range operatorDeploymentLabels { + for k, v := range operatorDeploymentLabels() { depLabels[k] = v podLabels[k] = v } diff --git a/pkg/iofog/install/local_agent.go b/pkg/iofog/install/local_agent.go deleted file mode 100644 index 04256e74d..000000000 --- a/pkg/iofog/install/local_agent.go +++ /dev/null @@ -1,88 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package install - -import ( - "fmt" - - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -// LocalAgent uses Container exec commands -type LocalAgent struct { - defaultAgent - client *LocalContainer - localAgentConfig *LocalAgentConfig -} - -func NewLocalAgent(localAgentConfig *LocalAgentConfig, client *LocalContainer) *LocalAgent { - return &LocalAgent{ - defaultAgent: defaultAgent{name: localAgentConfig.Name}, - localAgentConfig: localAgentConfig, - client: client, - } -} - -func (agent *LocalAgent) Bootstrap() error { - // Nothing to do for local agent, bootstraping is done inside the image. - return nil -} - -func (agent *LocalAgent) Configure(controllerEndpoint string, user IofogUser) (string, error) { - provisioningEndpoint := controllerEndpoint - if controllerEndpoint == "" || util.IsLocalHost(controllerEndpoint) { - localControllerEndpoint, err := agent.client.GetLocalControllerEndpoint() - if err != nil { - return "", err - } - provisioningEndpoint = "localhost:51121" - controllerEndpoint = localControllerEndpoint - } - - key, caCert, err := agent.getProvisionKey(provisioningEndpoint, user) - if err != nil { - return "", err - } - - // Instantiate provisioning commands - controllerBaseURL, err := util.GetBaseURL(controllerEndpoint) - if err != nil { - return "", err - } - cmds := [][]string{ - {"iofog-agent", "config", "-idc", "off"}, - {"iofog-agent", "config", "-a", controllerBaseURL.String()}, - } - - // Only add cert command if caCert is not empty - if caCert != "" { - cmds = append(cmds, []string{"iofog-agent", "cert", caCert}) - } - - cmds = append(cmds, []string{"iofog-agent", "provision", key}) - cmds = append(cmds, []string{"iofog-agent", "config", "-sf", "10", "-cf", "10"}) - - // Execute commands - for _, cmd := range cmds { - result, err := agent.client.ExecuteCmd(agent.localAgentConfig.ContainerName, cmd) - if result.ExitCode != 0 { - return "", util.NewError(fmt.Sprintf("Command: %v failed with exit code %d\nStdout: %s\n Stderr: %s\n", cmd, result.ExitCode, result.StdOut, result.StdErr)) - } - if err != nil { - return "", err - } - } - - return agent.uuid, nil -} diff --git a/pkg/iofog/install/local_container.go b/pkg/iofog/install/local_container.go index c1cb30903..7f64df532 100644 --- a/pkg/iofog/install/local_container.go +++ b/pkg/iofog/install/local_container.go @@ -1,48 +1,16 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package install import ( - "archive/tar" - "bytes" - "compress/gzip" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" "regexp" - "strconv" - "strings" "time" - "github.com/docker/docker/api/types" - dockerContainer "github.com/docker/docker/api/types/container" - img "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" - "github.com/docker/go-connections/nat" - "github.com/eclipse-iofog/iofogctl/pkg/iofog" - "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + dockengine "github.com/eclipse-iofog/iofogctl/pkg/containerengine/docker" ) // LocalContainer struct to encapsulate utilities around docker type LocalContainer struct { - client *client.Client + client *dockengine.Client } // ExecResult contains the output of a command ran into docker exec @@ -75,597 +43,119 @@ type LocalContainerConfig struct { Credentials Credentials } -type LocalControllerConfig struct { - ContainerMap map[string]*LocalContainerConfig - Database Database - PidBaseDir string - EcnViewerPort int - EcnViewerURL string - LogLevel string - Auth Auth -} - type LocalContainerPort struct { Protocol string Port string } -type LocalAgentConfig struct { - LocalContainerConfig - Name string -} - -func GetLocalContainerName(t string, isSystem bool) string { - names := map[string]string{ - "controller": sanitizeContainerName("iofog-controller"), - "agent": sanitizeContainerName("iofog-agent"), - } - name, ok := names[t] - if !ok { - return "" - } - if isSystem { - return name + "-system" - } - return name -} - func sanitizeContainerName(name string) string { r := regexp.MustCompile("[^a-zA-Z0-9_.-]") return r.ReplaceAllString(name, "-") } -// NewAgentConfig generates a static agent config -func NewLocalAgentConfig(name, image string, ctrlConfig *LocalContainerConfig, credentials Credentials, isSystem bool, timeZone string) *LocalAgentConfig { - if image == "" { - image = util.GetAgentImage() - } - - if ctrlConfig != nil && ctrlConfig.Host == "" { - ctrlConfig.Host = "0.0.0.0" - } - - if timeZone == "" { - timeZone = "Europe/Istanbul" - } - - return &LocalAgentConfig{ - LocalContainerConfig: LocalContainerConfig{ - Host: ctrlConfig.Host, - Ports: []port{ - {Host: "54321", Container: &LocalContainerPort{Protocol: "tcp", Port: "54321"}}, - }, - ContainerName: GetLocalContainerName("agent", isSystem), - Image: image, - Privileged: true, - Binds: []string{ - "/var/run/docker.sock:/var/run/docker.sock:rw", - "iofog-agent-config:/etc/iofog-agent:rw", - "iofog-agent-log:/var/log/iofog-agent:rw", - "iofog-agent-backup:/var/backups/iofog-agent:rw", - "iofog-agent-version:/usr/share/iofog-agent:rw", - "/var/lib/iofog-agent:/var/lib/iofog-agent:rw", - // "/sbin/shutdown:/sbin/shutdown", - }, - Envs: []string{ - "TZ=" + timeZone, - }, - NetworkMode: "host", - Credentials: credentials, - }, - Name: name, - } -} - -// LocalSystemImages optionally sets Router and Nats images and NATS enabling for the local controller. -type LocalSystemImages struct { - Router string - Nats string - NatsEnabled *bool // nil = default enabled -} - -// NewLocalControllerConfig generats a static controller config -func NewLocalControllerConfig(image string, credentials Credentials, auth Auth, db Database, events Events, systemImages *LocalSystemImages) *LocalContainerConfig { - if image == "" { - image = util.GetControllerImage() - } - - // Handle nil pointer fields safely - sslValue := "false" - if db.SSL != nil { - sslValue = strconv.FormatBool(*db.SSL) - } - - caValue := "" - if db.CA != nil { - caValue = *db.CA - } - - envs := []string{ - "CONTROL_PLANE=Remote", - "DB_PROVIDER=" + db.Provider, - "DB_HOST=" + db.Host, - "DB_USERNAME=" + db.User, - "DB_PASSWORD=" + db.Password, - "DB_PORT=" + strconv.Itoa(db.Port), - "DB_NAME=" + db.DatabaseName, - "DB_USE_SSL=" + sslValue, - "DB_SSL_CA=" + caValue, - "KC_URL=" + auth.URL, - "KC_REALM=" + auth.Realm, - "KC_SSL_REQ=" + auth.SSL, - "KC_REALM_KEY=" + auth.RealmKey, - "KC_CLIENT=" + auth.ControllerClient, - "KC_CLIENT_SECRET=" + auth.ControllerSecret, - "KC_VIEWER_CLIENT=" + auth.ViewerClient, - } - - // Add Events environment variables only if Events is explicitly configured - if events.AuditEnabled != nil { - // Always set EVENT_AUDIT_ENABLED (true or false) - envs = append(envs, fmt.Sprintf("EVENT_AUDIT_ENABLED=%t", *events.AuditEnabled)) - - // Set optional fields only if audit is enabled - if *events.AuditEnabled { - if events.RetentionDays != 0 { - envs = append(envs, fmt.Sprintf("EVENT_RETENTION_DAYS=%d", events.RetentionDays)) - } - if events.CleanupInterval != 0 { - envs = append(envs, fmt.Sprintf("EVENT_CLEANUP_INTERVAL=%d", events.CleanupInterval)) - } - // Set EVENT_CAPTURE_IP_ADDRESS if explicitly configured - if events.CaptureIpAddress != nil { - envs = append(envs, fmt.Sprintf("EVENT_CAPTURE_IP_ADDRESS=%t", *events.CaptureIpAddress)) - } - } - } - - if systemImages != nil { - if systemImages.Router != "" { - envs = append(envs, "ROUTER_IMAGE_1="+systemImages.Router, "ROUTER_IMAGE_2="+systemImages.Router) - } - natsImg := systemImages.Nats - if natsImg == "" { - natsImg = util.GetNatsImage() - } - if natsImg != "" { - envs = append(envs, "NATS_IMAGE_1="+natsImg, "NATS_IMAGE_2="+natsImg) - } - natsEnabled := true - if systemImages.NatsEnabled != nil { - natsEnabled = *systemImages.NatsEnabled - } - envs = append(envs, fmt.Sprintf("NATS_ENABLED=%t", natsEnabled)) - } - - return &LocalContainerConfig{ - Host: "0.0.0.0", - Ports: []port{ - {Host: iofog.ControllerPortString, Container: &LocalContainerPort{Port: iofog.ControllerPortString, Protocol: "tcp"}}, - {Host: iofog.ControllerHostECNViewerPortString, Container: &LocalContainerPort{Port: iofog.DefaultHTTPPortString, Protocol: "tcp"}}, - }, - ContainerName: GetLocalContainerName("controller", false), - Image: image, - Privileged: false, - Binds: []string{ - "iofog-controller-db:/home/runner/.npm-global/lib/node_modules/@eclipse-iofog/iofogcontroller/src/data/sqlite_files/:rw", - "iofog-controller-logs:/var/log/iofog-controller:rw", - }, - Envs: envs, - NetworkMode: "bridge", - Credentials: credentials, - } -} - -// NewLocalContainerClient returns a LocalContainer struct -func NewLocalContainerClient() (*LocalContainer, error) { - cli, err := client.NewClientWithOpts(client.WithAPIVersionNegotiation()) +// NewLocalContainerClient dials the container engine using ResolveContainerEngineURL. +func NewLocalContainerClient(engine string, agentCfg *client.AgentConfiguration) (*LocalContainer, error) { + cli, err := NewContainerEngineClient(engine, agentCfg) if err != nil { return nil, err } - if err := client.FromEnv(cli); err != nil { - return nil, err - } - return &LocalContainer{ - client: cli, - }, nil + return &LocalContainer{client: cli}, nil } // GetLogsByName returns the logs of the container specified by name func (lc *LocalContainer) GetLogsByName(name string) (stdout, stderr string, err error) { - ctx := context.Background() - r, err := lc.client.ContainerLogs(ctx, name, dockerContainer.LogsOptions{ShowStdout: true, ShowStderr: true}) - if err != nil { - return - } - defer r.Close() - - stdoutBuf := new(bytes.Buffer) - stderrBuf := new(bytes.Buffer) - - _, err = stdcopy.StdCopy(stdoutBuf, stderrBuf, r) - if err != nil { - return - } - - stdout = stdoutBuf.String() - stderr = stderrBuf.String() - - return + return lc.client.GetLogsByName(name) } -func (lc *LocalContainer) GetContainerByName(name string) (types.Container, error) { - ctx := context.Background() - // List containers - containers, err := lc.client.ContainerList(ctx, dockerContainer.ListOptions{}) - if err != nil { - return types.Container{}, err - } - - // Find by name - for idx := range containers { - container := &containers[idx] - for _, containerName := range container.Names { - if containerName == "/"+name { // Docker prefixes names with / - return *container, nil - } - } - } - return types.Container{}, util.NewInputError(fmt.Sprintf("Could not find container %s", name)) +// GetContainerByName returns a container summary by name. +func (lc *LocalContainer) GetContainerByName(name string) (dockengine.ContainerSummary, error) { + return lc.client.GetContainerByName(name) } -func (lc *LocalContainer) ListContainers() ([]types.Container, error) { - ctx := context.Background() - return lc.client.ContainerList(ctx, dockerContainer.ListOptions{}) +func (lc *LocalContainer) ListContainers() ([]dockengine.ContainerSummary, error) { + return lc.client.ListContainers(true) } // CleanContainer stops and remove a container based on a container name func (lc *LocalContainer) CleanContainer(name string) error { - ctx := context.Background() - - container, err := lc.GetContainerByName(name) - if err != nil { - return err - } - // Stop container if running (ignore error if there is no running container) - if err := lc.client.ContainerStop(ctx, container.ID, dockerContainer.StopOptions{"SIGTERM", nil}); err != nil { - return err - } - - // Force remove container - return lc.client.ContainerRemove(ctx, container.ID, dockerContainer.RemoveOptions{Force: true}) + return lc.client.RemoveContainerByName(name) } func (lc *LocalContainer) CleanContainerByID(id string) error { - ctx := context.Background() - - // Stop container if running (ignore error if there is no running container) - if err := lc.client.ContainerStop(ctx, id, dockerContainer.StopOptions{"SIGTERM", nil}); err != nil { - return err - } - - // Force remove container - return lc.client.ContainerRemove(ctx, id, dockerContainer.RemoveOptions{Force: true}) -} - -func (lc *LocalContainer) getPullOptions(config *LocalContainerConfig) (ret img.PullOptions) { - dockerUser := config.Credentials.User - dockerPwd := config.Credentials.Password - - if dockerUser != "" { - authConfig := registry.AuthConfig{ - Username: dockerUser, - Password: dockerPwd, - } - encodedJSON, err := json.Marshal(authConfig) - if err != nil { - panic(err) - } - authStr := base64.URLEncoding.EncodeToString(encodedJSON) - ret.RegistryAuth = authStr - } - return -} - -func getImageTag(image string) string { - if strings.HasPrefix(image, "docker.io/") { - return image[len("docker.io/"):] - } - return image -} - -func (lc *LocalContainer) waitForImage(image string, counter int8) error { - if counter >= 18 { // 180 seconds - return util.NewInternalError("Could not find newly pulled image: " + image) - } - ctx := context.Background() - imgs, listErr := lc.client.ImageList(ctx, img.ListOptions{All: true}) - if listErr != nil { - return util.NewError(fmt.Sprintf("Could not list local images: %v\n", listErr)) - } - for idx := range imgs { - for _, tag := range imgs[idx].RepoTags { - if tag == image { - return nil - } - } - } - time.Sleep(10 * time.Second) - return lc.waitForImage(image, counter+1) + return lc.client.RemoveContainerByID(id) } -// DeployContainer deploys a container based on an image and a port mappin +// DeployContainer deploys a container based on an image and a port mapping func (lc *LocalContainer) DeployContainer(containerConfig *LocalContainerConfig) (string, error) { - ctx := context.Background() - - portSet := nat.PortSet{} - portMap := nat.PortMap{} - - // Create port mappings - for _, port := range containerConfig.Ports { - natPort, err := nat.NewPort(port.Container.Protocol, port.Container.Port) - if err != nil { - return "", err + portBindings := map[string]dockengine.PortBinding{} + for _, p := range containerConfig.Ports { + proto := p.Container.Protocol + if proto == "" { + proto = "tcp" } - portSet[natPort] = struct{}{} - portMap[natPort] = []nat.PortBinding{ - { - HostIP: containerConfig.Host, - HostPort: port.Host, - }, + portBindings[p.Host] = dockengine.PortBinding{ + HostIP: containerConfig.Host, + HostPort: p.Host, + ContainerPort: p.Container.Port, + Protocol: proto, } } - dockerContainerConfig := &dockerContainer.Config{ - Image: containerConfig.Image, - ExposedPorts: portSet, - Env: containerConfig.Envs, - } - hostConfig := &dockerContainer.HostConfig{ - PortBindings: portMap, - Privileged: containerConfig.Privileged, + opts := dockengine.DeployOptions{ + Name: containerConfig.ContainerName, + Image: containerConfig.Image, + Env: containerConfig.Envs, Binds: containerConfig.Binds, - NetworkMode: dockerContainer.NetworkMode(containerConfig.NetworkMode), - RestartPolicy: dockerContainer.RestartPolicy{Name: dockerContainer.RestartPolicyAlways}, - } - - // Pull image - reader, err := lc.client.ImagePull(ctx, containerConfig.Image, lc.getPullOptions(containerConfig)) - imageTag := getImageTag(containerConfig.Image) - if err != nil { - Verbose(fmt.Sprintf("Could not pull image: %v, listing local images...\n", err.Error())) - imgs, listErr := lc.client.ImageList(ctx, img.ListOptions{All: true}) - if listErr != nil { - Verbose(fmt.Sprintf("Could not list local images: %v\n", listErr)) - return "", err - } - found := false - for idx := range imgs { - for _, tag := range imgs[idx].RepoTags { - if tag == imageTag { - found = true - break - } - } - if found { - break - } - } - if !found { - Verbose(fmt.Sprintf("Could not pull image: %v\n Could not find image [%v] locally, please run docker pull [%v]\n", err, containerConfig.Image, containerConfig.Image)) - return "", err - } - } else { - defer reader.Close() - _, err := io.ReadAll(reader) - if err != nil { - return "", err - } - // Wait for image to be discoverable by docker daemon - err = lc.waitForImage(imageTag, 0) - if err != nil { - return "", err - } - } - - container, err := lc.client.ContainerCreate(ctx, dockerContainerConfig, hostConfig, nil, nil, containerConfig.ContainerName) - if err != nil { - return "", util.NewError(fmt.Sprintf("Failed to create container: %v\n", err)) - } - - // Start container - err = lc.client.ContainerStart(ctx, container.ID, dockerContainer.StartOptions{}) - if err != nil { - return "", util.NewError(fmt.Sprintf("Failed to start container: %v\n", err)) + Privileged: containerConfig.Privileged, + NetworkMode: containerConfig.NetworkMode, + PortBindings: portBindings, + RestartPolicy: dockengine.RestartPolicyAlways, + StopTimeout: 60 * time.Second, } - - return container.ID, err -} - -// Returns endpoint to reach controller container from within another container -func (lc *LocalContainer) GetLocalControllerEndpoint() (controllerEndpoint string, err error) { - host, err := lc.GetContainerIP(GetLocalContainerName("controller", false)) - if err != nil { - return controllerEndpoint, err + pullOpts := dockengine.PullOptions{ + Username: containerConfig.Credentials.User, + Password: containerConfig.Credentials.Password, } - controllerEndpoint = fmt.Sprintf("http://%s:%s", host, iofog.ControllerPortString) - return + return lc.client.DeployContainer(opts, pullOpts) } func (lc *LocalContainer) GetContainerIP(name string) (ip string, err error) { - container, err := lc.GetContainerByName(name) - if err != nil { - return - } - - network, found := container.NetworkSettings.Networks[container.HostConfig.NetworkMode] - if !found { - return "", util.NewNotFoundError(fmt.Sprintf("Container %s : Could not find network setting for network %s", name, container.HostConfig.NetworkMode)) - } - - return network.IPAddress, nil + return lc.client.GetContainerIP(name) } func (lc *LocalContainer) WaitForCommand(containerName string, condition *regexp.Regexp, command ...string) error { - for iteration := 0; iteration < 120; iteration++ { - output, err := lc.ExecuteCmd(containerName, command) - if err != nil { - Verbose(fmt.Sprintf("Container command %v failed with error %v\n", command, err.Error())) - } - if condition.MatchString(output.StdOut) { - return nil - } - time.Sleep(2 * time.Second) - } - return util.NewInternalError("Timed out waiting for container") + return lc.client.WaitForCommand(containerName, func(stdout string) bool { + return condition.MatchString(stdout) + }, command...) } func (lc *LocalContainer) ExecuteCmd(name string, cmd []string) (execResult ExecResult, err error) { - ctx := context.Background() - - container, err := lc.GetContainerByName(name) - if err != nil { - return - } - - // Create command to execute inside container - execConfig := dockerContainer.ExecOptions{AttachStdout: true, AttachStderr: true, - Cmd: cmd} - execStartCheck := dockerContainer.ExecStartOptions{} - - execID, err := lc.client.ContainerExecCreate(ctx, container.ID, execConfig) - if err != nil { - return - } - - // Attach command to container - res, err := lc.client.ContainerExecAttach(ctx, execID.ID, execStartCheck) - if err != nil { - return - } - defer res.Close() - - // read the output - var outBuf, errBuf bytes.Buffer - outputDone := make(chan error) - - go func() { - // StdCopy demultiplexes the stream into two buffers - _, err = stdcopy.StdCopy(&outBuf, &errBuf, res.Reader) - outputDone <- err - }() - - select { - case err := <-outputDone: - if err != nil { - return execResult, err - } - break - - case <-ctx.Done(): - return execResult, ctx.Err() - } - - stdout, err := io.ReadAll(&outBuf) - if err != nil { - return execResult, err - } - stderr, err := io.ReadAll(&errBuf) - if err != nil { - return execResult, err - } - - inspect, err := lc.client.ContainerExecInspect(ctx, execID.ID) + result, err := lc.client.ExecuteCmd(name, cmd) if err != nil { return execResult, err } - - execResult.ExitCode = inspect.ExitCode - execResult.StdOut = string(stdout) - execResult.StdErr = string(stderr) - - // Run command - if err = lc.client.ContainerExecStart(ctx, execID.ID, execStartCheck); err != nil { - return - } - return execResult, nil + return ExecResult{ + StdOut: result.StdOut, + StdErr: result.StdErr, + ExitCode: result.ExitCode, + }, nil } -func compress(src string, buf io.Writer) error { - // tar > gzip > buf - zr := gzip.NewWriter(buf) - tw := tar.NewWriter(zr) - - srcLength := len(filepath.ToSlash(src)) - - // walk through every file in the folder - err := filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { - if err != nil { - return err - } - if file == src { - // Skip root folder - return nil - } - // generate tar header - header, err := tar.FileInfoHeader(fi, file) - if err != nil { - return err - } +func (lc *LocalContainer) CopyToContainer(name, source, dest string) (err error) { + return lc.client.CopyToContainer(name, source, dest) +} - // must provide relative name. Get everything after the source - name := string([]rune(filepath.ToSlash(file))[srcLength:]) - header.Name = name +// EdgeletContainerName is the default docker container name for edgelet. +const EdgeletContainerName = "edgelet" - // write header - if err := tw.WriteHeader(header); err != nil { - return err - } - // if not a dir, write file content - if !fi.IsDir() { - data, err := os.Open(file) - if err != nil { - return err - } - if _, err := io.Copy(tw, data); err != nil { - return err - } - } +func edgeletAgentSDKConfig(cfg EdgeletInstallConfig) *client.AgentConfiguration { + if cfg.Runtime == nil { return nil - }) - if err != nil { - return err - } - - // produce tar - if err := tw.Close(); err != nil { - return err - } - // produce gzip - if err := zr.Close(); err != nil { - return err } - // - return nil + return &cfg.Runtime.Agent } -func (lc *LocalContainer) CopyToContainer(name, source, dest string) (err error) { - ctx := context.Background() - - container, err := lc.GetContainerByName(name) - if err != nil { - return - } - - // content must be a Reader to a TAR - // tar + gzip - var content bytes.Buffer - _ = compress(source, &content) - - // Create dest folder in container if not exists - if _, err = lc.ExecuteCmd(name, []string{"mkdir", "-p", dest}); err != nil { - return err - } - - return lc.client.CopyToContainer(ctx, container.ID, dest, &content, types.CopyToContainerOptions{}) +// NewLocalContainerClientFromEdgeletCfg dials the engine configured for an edgelet install. +func NewLocalContainerClientFromEdgeletCfg(cfg EdgeletInstallConfig) (*LocalContainer, error) { + return NewLocalContainerClient(cfg.containerEngine(), edgeletAgentSDKConfig(cfg)) } diff --git a/pkg/iofog/install/paths.go b/pkg/iofog/install/paths.go new file mode 100644 index 000000000..51d3c9bf3 --- /dev/null +++ b/pkg/iofog/install/paths.go @@ -0,0 +1,32 @@ +package install + +import "strings" + +// EdgeletShareDir returns the canonical on-host script directory for an edgelet host OS. +func EdgeletShareDir(hostOS string) string { + switch normalizeEdgeletShareHostOS(hostOS) { + case "darwin": + return "/usr/local/share/edgelet" + case "windows": + return `%ProgramData%\Edgelet\scripts` + default: + return "/usr/share/edgelet" + } +} + +// EdgeletScriptStageDir is the transient directory used before publishing to EdgeletShareDir. +const EdgeletScriptStageDir = "/tmp/edgelet-scripts" + +// EdgeletContainerManifestDir is bind-mounted into the edgelet container on desktop deploys. +const EdgeletContainerManifestDir = "/tmp/edgelet" + +func normalizeEdgeletShareHostOS(hostOS string) string { + switch strings.ToLower(strings.TrimSpace(hostOS)) { + case "darwin", "macos", "osx": + return "darwin" + case "windows", "windows_nt", "win32": + return "windows" + default: + return "linux" + } +} diff --git a/pkg/iofog/install/pkg.go b/pkg/iofog/install/pkg.go index 50cdf5119..d34a38cf5 100644 --- a/pkg/iofog/install/pkg.go +++ b/pkg/iofog/install/pkg.go @@ -1,52 +1,43 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package install var pkg struct { - scriptPrereq string - scriptInit string - scriptInstallDeps string - scriptInstallJava string - scriptInstallContainerEngine string - scriptInstallIofog string - scriptUninstallIofog string - controllerScriptPrereq string - controllerScriptInit string - controllerScriptInstallContainerEngine string - controllerScriptSetEnv string - controllerScriptInstall string - controllerScriptUninstall string + edgeletScriptPrereq string + edgeletScriptDetectInit string + edgeletScriptInstallDeps string + edgeletScriptConfigureContainerEngine string + edgeletScriptInstall string + edgeletScriptInstallContainer string + edgeletScriptInstallInitUnits string + edgeletScriptStartEdgelet string + edgeletScriptConfigureContainerEdgelet string + edgeletScriptWaitEdgeletReady string + edgeletScriptBundled string + edgeletScriptUninstall string + edgeletLibScripts []string iofogDir string - agentDir string - controllerDir string } func init() { - pkg.scriptPrereq = "check_prereqs.sh" - pkg.scriptInit = "init.sh" - pkg.scriptInstallDeps = "install_deps.sh" - pkg.scriptInstallJava = "install_java.sh" - pkg.scriptInstallContainerEngine = "install_container_engine.sh" - pkg.scriptInstallIofog = "install_iofog.sh" - pkg.scriptUninstallIofog = "uninstall_iofog.sh" - pkg.controllerScriptPrereq = "check_prereqs.sh" - pkg.controllerScriptInit = "init.sh" - pkg.controllerScriptInstallContainerEngine = "install_container_engine.sh" - pkg.controllerScriptSetEnv = "set_env.sh" - pkg.controllerScriptInstall = "install_iofog.sh" - pkg.controllerScriptUninstall = "uninstall_iofog.sh" + pkg.edgeletScriptPrereq = "check_prereqs.sh" + pkg.edgeletScriptDetectInit = "detect_init.sh" + pkg.edgeletScriptInstallDeps = "install_deps.sh" + pkg.edgeletScriptConfigureContainerEngine = "configure_container_engine.sh" + pkg.edgeletScriptInstall = "install.sh" + pkg.edgeletScriptInstallContainer = "install_container.sh" + pkg.edgeletScriptInstallInitUnits = "install_init_units.sh" + pkg.edgeletScriptStartEdgelet = "start_edgelet.sh" + pkg.edgeletScriptConfigureContainerEdgelet = "configure_container_edgelet.sh" + pkg.edgeletScriptWaitEdgeletReady = "wait_edgelet_ready.sh" + pkg.edgeletScriptBundled = "bundled.sh" + pkg.edgeletScriptUninstall = "uninstall.sh" + pkg.edgeletLibScripts = []string{ + "lib/common.sh", + "lib/paths.sh", + "lib/receipt.sh", + "lib/binary.sh", + "lib/container_cli.sh", + "lib/container_engine.sh", + "lib/container_mounts.sh", + } pkg.iofogDir = "/etc/iofog" - pkg.agentDir = "/etc/iofog/agent" - pkg.controllerDir = "/etc/iofog/controller" } diff --git a/pkg/iofog/install/procedures.go b/pkg/iofog/install/procedures.go new file mode 100644 index 000000000..6ccae97b6 --- /dev/null +++ b/pkg/iofog/install/procedures.go @@ -0,0 +1,50 @@ +package install + +import ( + "fmt" + "strings" +) + +// AgentProcedures holds layered install script entrypoints shared by edgelet and controller flows. +type AgentProcedures struct { + check Entrypoint `yaml:"-"` + Deps Entrypoint `yaml:"deps,omitempty"` + Install Entrypoint `yaml:"install,omitempty"` + Uninstall Entrypoint `yaml:"uninstall,omitempty"` + scriptNames []string `yaml:"-"` + scriptContents []string `yaml:"-"` +} + +// Entrypoint describes one embedded or custom install script invocation. +type Entrypoint struct { + Name string `yaml:"entrypoint"` + Args []string `yaml:"args"` + destPath string `yaml:"-"` +} + +func (script *Entrypoint) getCommand() string { + if script.destPath == "" { + return "" + } + if len(script.Args) == 0 { + return script.destPath + } + return fmt.Sprintf("%s %s", script.destPath, shellJoinArgs(script.Args)) +} + +func shellJoinArgs(args []string) string { + quoted := make([]string, len(args)) + for i, arg := range args { + quoted[i] = shellQuoteArg(arg) + } + return strings.Join(quoted, " ") +} + +func shellQuoteArg(arg string) string { + return "'" + strings.ReplaceAll(arg, "'", `'"'"'`) + "'" +} + +type command struct { + cmd string + msg string +} diff --git a/pkg/iofog/install/procedures_test.go b/pkg/iofog/install/procedures_test.go new file mode 100644 index 000000000..f0a38dfb8 --- /dev/null +++ b/pkg/iofog/install/procedures_test.go @@ -0,0 +1,15 @@ +package install + +import "testing" + +func TestEntrypointGetCommandQuotesInstallArgs(t *testing.T) { + ep := Entrypoint{ + destPath: "/tmp/edgelet-scripts/install.sh", + Args: []string{"--version=v1.0.0-rc.6", "--arch=arm64", "--skip-start"}, + } + got := ep.getCommand() + want := "/tmp/edgelet-scripts/install.sh '--version=v1.0.0-rc.6' '--arch=arm64' '--skip-start'" + if got != want { + t.Fatalf("getCommand() = %q, want %q", got, want) + } +} diff --git a/pkg/iofog/install/remote_agent.go b/pkg/iofog/install/remote_agent.go deleted file mode 100644 index 6f0a7e0e4..000000000 --- a/pkg/iofog/install/remote_agent.go +++ /dev/null @@ -1,620 +0,0 @@ -/* -* ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * -*/ - -package install - -import ( - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/pkg/util" -) - -// Remote agent uses SSH -type RemoteAgent struct { - defaultAgent - ssh *util.SecureShellClient - version string - // repo string - // token string - dir string - procs AgentProcedures - customInstall bool // Flag set when custom install scripts are provided - airgap bool // Flag set when airgap deployment is enabled -} - -type AgentProcedures struct { - check Entrypoint `yaml:"-"` // Check prereqs script (runs for default and custom procedures) - Deps Entrypoint `yaml:"deps,omitempty"` - Install Entrypoint `yaml:"install,omitempty"` - Uninstall Entrypoint `yaml:"uninstall,omitempty"` - scriptNames []string `yaml:"-"` // List of all script names to be pushed to Agent - scriptContents []string `yaml:"-"` // List of contents of scripts to be pushed to Agent -} - -type Entrypoint struct { - Name string `yaml:"entrypoint"` - Args []string `yaml:"args"` - destPath string `yaml:"-"` // Dir + filename on Agent -} - -func (script *Entrypoint) getCommand() string { - args := strings.Join(script.Args, " ") - return fmt.Sprintf("%s %s", script.destPath, args) -} - -func NewRemoteAgent(user, host string, port int, privKeyFilename, agentName, agentUUID string) (*RemoteAgent, error) { - ssh, err := util.NewSecureShellClient(user, host, privKeyFilename) - if err != nil { - return nil, err - } - ssh.SetPort(port) - agent := &RemoteAgent{ - defaultAgent: defaultAgent{name: agentName, uuid: agentUUID}, - ssh: ssh, - version: util.GetAgentVersion(), - dir: pkg.agentDir, - procs: AgentProcedures{ - check: Entrypoint{ - Name: pkg.scriptPrereq, - destPath: util.JoinAgentPath(pkg.agentDir, pkg.scriptPrereq), - }, - Deps: Entrypoint{ - Name: pkg.scriptInstallDeps, - destPath: util.JoinAgentPath(pkg.agentDir, pkg.scriptInstallDeps), - }, - Install: Entrypoint{ - Name: pkg.scriptInstallIofog, - destPath: util.JoinAgentPath(pkg.agentDir, pkg.scriptInstallIofog), - Args: []string{ - util.GetAgentVersion(), - "", - "", - }, - }, - Uninstall: Entrypoint{ - Name: pkg.scriptUninstallIofog, - destPath: util.JoinAgentPath(pkg.agentDir, pkg.scriptUninstallIofog), - }, - scriptNames: []string{ - pkg.scriptPrereq, - pkg.scriptInit, - pkg.scriptInstallDeps, - pkg.scriptInstallJava, - pkg.scriptInstallContainerEngine, - pkg.scriptInstallIofog, - pkg.scriptUninstallIofog, - }, - }, - } - // Get script contents from embedded files - for _, scriptName := range agent.procs.scriptNames { - scriptContent, err := util.GetStaticFile(addAgentAssetPrefix(scriptName)) - if err != nil { - return nil, err - } - agent.procs.scriptContents = append(agent.procs.scriptContents, scriptContent) - } - return agent, nil -} - -func NewRemoteContainerAgent(user, host string, port int, privKeyFilename, agentName, agentUUID, agentTZ string) (*RemoteAgent, error) { - ssh, err := util.NewSecureShellClient(user, host, privKeyFilename) - if err != nil { - return nil, err - } - ssh.SetPort(port) - if agentTZ == "" { - agentTZ = "Europe/Istanbul" - } - agent := &RemoteAgent{ - defaultAgent: defaultAgent{name: agentName, uuid: agentUUID}, - ssh: ssh, - version: util.GetAgentVersion(), - dir: pkg.agentDir, - procs: AgentProcedures{ - check: Entrypoint{ - Name: pkg.scriptPrereq, - destPath: util.JoinAgentPath(pkg.agentDir, pkg.scriptPrereq), - }, - Deps: Entrypoint{ - Name: pkg.scriptInstallDeps, - destPath: util.JoinAgentPath(pkg.agentDir, pkg.scriptInstallDeps), - }, - Install: Entrypoint{ - Name: pkg.scriptInstallIofog, - destPath: util.JoinAgentPath(pkg.agentDir, pkg.scriptInstallIofog), - Args: []string{ - util.GetAgentImage(), - agentTZ, - "", - }, - }, - Uninstall: Entrypoint{ - Name: pkg.scriptUninstallIofog, - destPath: util.JoinAgentPath(pkg.agentDir, pkg.scriptUninstallIofog), - }, - scriptNames: []string{ - pkg.scriptPrereq, - pkg.scriptInit, - pkg.scriptInstallDeps, - pkg.scriptInstallContainerEngine, - pkg.scriptInstallIofog, - pkg.scriptUninstallIofog, - }, - }, - } - // Get script contents from embedded files - for _, scriptName := range agent.procs.scriptNames { - scriptContent, err := util.GetStaticFile(agent.addContainerAgentAssetPrefix(scriptName)) - if err != nil { - return nil, err - } - agent.procs.scriptContents = append(agent.procs.scriptContents, scriptContent) - } - return agent, nil -} - -func (agent *RemoteAgent) CustomizeProcedures(dir string, procs *AgentProcedures) error { - // Format source directory of script files - dir, err := util.FormatPath(dir) - if err != nil { - return err - } - - // Load script files into memory - files, err := os.ReadDir(dir) - if err != nil { - return err - } - for _, file := range files { - if !file.IsDir() { - procs.scriptNames = append(procs.scriptNames, file.Name()) - content, err := os.ReadFile(filepath.Join(dir, file.Name())) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, string(content)) - } - } - - // Add prereq script and entrypoint - procs.scriptNames = append(procs.scriptNames, pkg.scriptPrereq) - prereqContent, err := util.GetStaticFile(addAgentAssetPrefix(pkg.scriptPrereq)) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, prereqContent) - procs.check.destPath = util.JoinAgentPath(agent.dir, pkg.scriptPrereq) - - // Add default entrypoints and scripts if necessary (user not provided) - if procs.Deps.Name == "" { - procs.Deps = agent.procs.Deps - for _, script := range []string{pkg.scriptInstallDeps, pkg.scriptInstallContainerEngine, pkg.scriptInstallJava} { - procs.scriptNames = append(procs.scriptNames, script) - scriptContent, err := util.GetStaticFile(addAgentAssetPrefix(script)) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, scriptContent) - } - } - if procs.Install.Name == "" { - procs.Install = agent.procs.Install - procs.scriptNames = append(procs.scriptNames, pkg.scriptInstallIofog) - scriptContent, err := util.GetStaticFile(addAgentAssetPrefix(pkg.scriptInstallIofog)) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, scriptContent) - } else { - agent.customInstall = true - } - if procs.Uninstall.Name == "" { - procs.Uninstall = agent.procs.Uninstall - procs.scriptNames = append(procs.scriptNames, pkg.scriptUninstallIofog) - scriptContent, err := util.GetStaticFile(addAgentAssetPrefix(pkg.scriptUninstallIofog)) - if err != nil { - return err - } - procs.scriptContents = append(procs.scriptContents, scriptContent) - } - - // Set destination paths where scripts appear on Agent - procs.Deps.destPath = util.JoinAgentPath(agent.dir, procs.Deps.Name) - procs.Install.destPath = util.JoinAgentPath(agent.dir, procs.Install.Name) - procs.Uninstall.destPath = util.JoinAgentPath(agent.dir, procs.Uninstall.Name) - - agent.procs = *procs - return nil -} - -func (agent *RemoteAgent) SetVersion(version string) { - if version == "" || agent.customInstall { - return - } - agent.version = version - agent.procs.Install.Args[0] = version -} - -func (agent *RemoteAgent) SetContainerImage(image string) { - if image == "" || agent.customInstall { - return - } - agent.procs.Install.Args[0] = image -} - -func (agent *RemoteAgent) SetAirgap(airgap bool) { - agent.airgap = airgap -} - -// func (agent *RemoteAgent) SetRepository(repo, token string) { -// if repo == "" || agent.customInstall { -// return -// } -// agent.repo = repo -// agent.procs.Install.Args[1] = repo -// agent.token = token -// agent.procs.Install.Args[2] = token -// } - -func (agent *RemoteAgent) Bootstrap() error { - // Prepare Agent for bootstrap - if err := agent.copyInstallScriptsToAgent(); err != nil { - return err - } - - // Define bootstrap commands - cmds := []command{ - { - cmd: agent.procs.check.getCommand(), - msg: "Checking prerequisites on Agent " + agent.name, - }, - { - cmd: agent.procs.Deps.getCommand(), - msg: "Installing dependancies on Agent " + agent.name, - }, - { - cmd: fmt.Sprintf("sudo %s", agent.procs.Install.getCommand()), - msg: "Installing ioFog daemon on Agent " + agent.name, - }, - } - - // Execute commands on remote server - if err := agent.run(cmds); err != nil { - return err - } - - return nil -} - -func (agent *RemoteAgent) Configure(controllerEndpoint string, user IofogUser) (string, error) { - key, caCert, err := agent.getProvisionKey(controllerEndpoint, user) - if err != nil { - return "", err - } - - controllerBaseURL, err := util.GetBaseURL(controllerEndpoint) - if err != nil { - return "", err - } - // Instantiate commands - cmds := []command{ - { - cmd: "sudo iofog-agent config -a " + controllerBaseURL.String(), - msg: "Configuring Agent " + agent.name + " with Controller URL " + controllerBaseURL.String(), - }, - } - - // Only add cert command if caCert is not empty - if caCert != "" { - cmds = append(cmds, command{ - cmd: "sudo iofog-agent cert " + caCert, - msg: "Configuring Agent " + agent.name + " with CA Certificate", - }) - } - - cmds = append(cmds, command{ - cmd: "sudo iofog-agent provision " + key, - msg: "Provisioning Agent " + agent.name + " with Controller", - }) - - // Execute commands on remote server - if err := agent.run(cmds); err != nil { - return "", err - } - - return agent.uuid, nil -} - -func (agent *RemoteAgent) SetInitialConfig( - name, fogType string, - // latitude, longitude float64, - // description, fogType string, - agentConfig client.AgentConfiguration, -) error { - // Prepare the base commands for agent configuration - cmds := []command{} - - // Convert FogType (string) to required format if necessary - fogTypeValue := fogType - if fogType == "" { - fogTypeValue = "auto" // Default value if fogType is empty - } - - // Convert WatchdogEnabled (*bool) to "on"/"off" - watchdogEnabled := "off" - if agentConfig.WatchdogEnabled != nil && *agentConfig.WatchdogEnabled { - watchdogEnabled = "on" - } - - // // Format GPS coordinates (Latitude and Longitude) - // gpsCoordinates := "" - // if latitude != 0 || longitude != 0 { - // gpsCoordinates = fmt.Sprintf("%f,%f", latitude, longitude) - // } - - // Extract values from agentConfig and construct options - configOptions := map[string]string{ - "-ft": fogTypeValue, - // "-gps": gpsCoordinates, - } - - // Add values from agentConfig to configOptions, properly handling pointers - if agentConfig.NetworkInterface != nil && *agentConfig.NetworkInterface != "" { - configOptions["-n"] = *agentConfig.NetworkInterface - } - if agentConfig.DockerURL != nil && *agentConfig.DockerURL != "" { - configOptions["-c"] = *agentConfig.DockerURL - } - if agentConfig.DiskLimit != nil { - configOptions["-d"] = strconv.FormatInt(*agentConfig.DiskLimit, 10) - } - if agentConfig.DiskDirectory != nil && *agentConfig.DiskDirectory != "" { - configOptions["-dl"] = *agentConfig.DiskDirectory - } - if agentConfig.MemoryLimit != nil { - configOptions["-m"] = strconv.FormatInt(*agentConfig.MemoryLimit, 10) - } - if agentConfig.CPULimit != nil { - configOptions["-p"] = strconv.FormatInt(*agentConfig.CPULimit, 10) - } - if agentConfig.LogLimit != nil { - configOptions["-l"] = strconv.FormatInt(*agentConfig.LogLimit, 10) - } - if agentConfig.LogDirectory != nil && *agentConfig.LogDirectory != "" { - configOptions["-ld"] = *agentConfig.LogDirectory - } - if agentConfig.LogFileCount != nil { - configOptions["-lc"] = strconv.FormatInt(*agentConfig.LogFileCount, 10) - } - if agentConfig.StatusFrequency != nil { - configOptions["-sf"] = strconv.FormatFloat(*agentConfig.StatusFrequency, 'f', -1, 64) - } - if agentConfig.ChangeFrequency != nil { - configOptions["-cf"] = strconv.FormatFloat(*agentConfig.ChangeFrequency, 'f', -1, 64) - } - if agentConfig.DeviceScanFrequency != nil { - configOptions["-sd"] = strconv.FormatFloat(*agentConfig.DeviceScanFrequency, 'f', -1, 64) - } - if agentConfig.LogLevel != nil && *agentConfig.LogLevel != "" { - configOptions["-ll"] = *agentConfig.LogLevel - } - if agentConfig.AvailableDiskThreshold != nil { - configOptions["-dt"] = strconv.FormatFloat(*agentConfig.AvailableDiskThreshold, 'f', -1, 64) - } - if agentConfig.DockerPruningFrequency != nil { - configOptions["-pf"] = strconv.FormatFloat(*agentConfig.DockerPruningFrequency, 'f', -1, 64) - } - // if agentConfig.GpsDevice != nil && *agentConfig.GpsDevice != "" { - // configOptions["-gpsd"] = *agentConfig.GpsDevice - // } - // if agentConfig.GpsMode != nil && *agentConfig.GpsMode != "" { - // configOptions["-gps"] = *agentConfig.GpsMode - // } - // if agentConfig.GpsScanFrequency != nil { - // configOptions["-gpsf"] = strconv.FormatFloat(*agentConfig.GpsScanFrequency, 'f', -1, 64) - // } - // if agentConfig.EdgeGuardFrequency != nil { - // configOptions["-egf"] = strconv.FormatFloat(*agentConfig.EdgeGuardFrequency, 'f', -1, 64) - // } - if agentConfig.TimeZone != "" { - configOptions["-tz"] = agentConfig.TimeZone - } - - // Add watchdogEnabled to config options - configOptions["-idc"] = watchdogEnabled - - // Iterate through the configOptions and add commands for non-empty values - for option, value := range configOptions { - if value != "" { - cmds = append(cmds, command{ - cmd: fmt.Sprintf("sudo iofog-agent config %s %s", option, value), - msg: fmt.Sprintf("Configuring Agent %s with option %s and value %s", name, option, value), - }) - } - } - - // If no commands were generated, return an error - if len(cmds) == 0 { - return fmt.Errorf("no valid configuration options provided for the agent") - } - - // Execute commands on the remote server - if err := agent.run(cmds); err != nil { - return err - } - - return nil -} - -func (agent *RemoteAgent) Deprovision() (err error) { - // Prepare commands - cmds := []command{ - { - cmd: "sudo iofog-agent deprovision", - msg: "Deprovisioning Agent " + agent.name, - }, - } - - // Execute commands on remote server - if err = agent.run(cmds); err != nil && !isNotProvisionedError(err) { - return - } - - return -} - -func (agent *RemoteAgent) Stop() (err error) { - // Prepare commands - cmds := []command{ - { - cmd: "sudo iofog-agent deprovision", - msg: "Deprovisioning Agent " + agent.name, - }, - } - if err = agent.run(cmds); err != nil && !isNotProvisionedError(err) { - return err - } - - cmds = []command{ - { - cmd: "sudo -S service iofog-agent stop", - msg: "Stopping Agent " + agent.name, - }, - } - if err := agent.run(cmds); err != nil { - return err - } - - return -} - -func isNotProvisionedError(err error) bool { - return strings.Contains(err.Error(), "not provisioned") -} - -func (agent *RemoteAgent) Prune() (err error) { - // Prepare commands - cmds := []command{ - { - // cmd: "sudo -S service iofog-agent prune", - cmd: "sudo -S iofog-agent prune", - msg: "Pruning Agent " + agent.name, - }, - } - - // Execute commands on remote server - if err := agent.run(cmds); err != nil { - return err - } - - return -} - -func (agent *RemoteAgent) Uninstall() (err error) { - // Stop iofog-agent properly - if err = agent.Stop(); err != nil { - return - } - - // Prepare commands - cmds := []command{ - // TODO: Implement purge on agent - // { - // cmd: "sudo iofog-agent purge", - // msg: "Deprovisioning Agent " + agent.name, - // }, - { - cmd: agent.procs.Uninstall.getCommand(), - msg: "Removing iofog-agent software " + agent.name, - }, - } - - // Execute commands on remote server - if err = agent.run(cmds); err != nil { - return - } - - return -} - -func (agent *RemoteAgent) run(cmds []command) (err error) { - // Establish SSH to agent - if err = agent.ssh.Connect(); err != nil { - return - } - defer util.Log(agent.ssh.Disconnect) - - // Execute commands - for _, cmd := range cmds { - Verbose(cmd.msg) - if _, err = agent.ssh.Run(cmd.cmd); err != nil { - return err - } - } - - return -} - -func (agent *RemoteAgent) copyInstallScriptsToAgent() error { - Verbose("Copying install scripts to Agent " + agent.name) - cmds := []command{ - { - cmd: fmt.Sprintf("sudo mkdir -p %s && sudo chmod -R 0777 %s", agent.dir, agent.dir), - msg: "Creating Agent etc directory", - }, - } - if err := agent.run(cmds); err != nil { - return err - } - return agent.copyScriptsToAgent() -} - -func (agent *RemoteAgent) copyScriptsToAgent() error { - // Establish SSH to agent - if err := agent.ssh.Connect(); err != nil { - return err - } - defer util.Log(agent.ssh.Disconnect) - - // Copy scripts to remote host - for idx, script := range agent.procs.scriptNames { - content := agent.procs.scriptContents[idx] - reader := strings.NewReader(content) - if err := agent.ssh.CopyTo(reader, agent.dir, script, "0775", int64(len(content))); err != nil { - return err - } - } - return nil -} - -func addAgentAssetPrefix(file string) string { - return fmt.Sprintf("agent/%s", file) -} - -func (agent *RemoteAgent) addContainerAgentAssetPrefix(file string) string { - if agent.airgap { - return fmt.Sprintf("airgap-agent/%s", file) - } - return fmt.Sprintf("container-agent/%s", file) -} - -type command struct { - cmd string - msg string -} diff --git a/pkg/iofog/install/remote_agent_test.go b/pkg/iofog/install/remote_agent_test.go deleted file mode 100644 index 88fc4cc16..000000000 --- a/pkg/iofog/install/remote_agent_test.go +++ /dev/null @@ -1,175 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package install - -import ( - "os" - "path" - "testing" -) - -func copyDir(src, dst string) (err error) { - files, err := os.ReadDir(src) - if err != nil { - return - } - for _, file := range files { - if err = copyFile(path.Join(src, file.Name()), path.Join(dst, file.Name())); err != nil { - return - } - } - return -} - -func copyFile(src, dst string) (err error) { - input, err := os.ReadFile(src) - if err != nil { - return - } - - err = os.WriteFile(dst, input, 0644) - if err != nil { - return - } - return -} - -type testState struct { - user string - host string - port int - keyFile string - agentName string - agentUUID string - dir string - srcDir string - procs AgentProcedures -} - -var state = testState{ - user: "serge", - host: "localhost", - port: 51121, - keyFile: "~/.ssh/id_rsa", - agentName: "albert", - agentUUID: "ashdifafhsdiofd", - srcDir: "../../../assets/agent", - dir: "/tmp/iofogctl-test-go", -} - -func runTest(t *testing.T, state testState) { - agent := RemoteAgent{} - - if err := agent.CustomizeProcedures(state.dir, &state.procs); err != nil { - t.Fatalf("Failed to customize procedures: %s", err.Error()) - } - - expectFiles, err := os.ReadDir(state.srcDir) - if err != nil { - t.Fatalf("Failed to count files in src script dir: %s", err.Error()) - } - expect := len(expectFiles) - if len(agent.procs.scriptNames) != expect { - t.Fatalf("Expected %d scripts names, found %d %v", expect, len(agent.procs.scriptNames), agent.procs.scriptNames) - if len(agent.procs.scriptContents) != len(agent.procs.scriptNames) { - t.Fatalf("Expected %d scripts contents, found %d", len(agent.procs.scriptNames), len(agent.procs.scriptContents)) - } - for idx, filename := range agent.procs.scriptNames { - fileBytes, err := os.ReadFile(filename) - if err != nil { - t.Fatalf("Failed to read script %s: %s", filename, err.Error()) - } - if string(fileBytes) != agent.procs.scriptContents[idx] { - t.Fatalf("Script contents for %s are not correct", filename) - } - } - } -} - -func generateScripts(t *testing.T, rm []string) { - os.RemoveAll(state.dir) - if err := os.MkdirAll(state.dir, os.FileMode(0777)); err != nil { - t.Fatalf("Failed to create dir: %s", err.Error()) - } - if err := copyDir(state.srcDir, state.dir); err != nil { - t.Fatalf("Failed to copy dir: %s", err.Error()) - } - if err := os.Remove(path.Join(state.dir, pkg.scriptPrereq)); err != nil { - t.Fatalf("Failed to delete %s: %s", pkg.scriptPrereq, err.Error()) - } - for _, file := range rm { - if err := os.Remove(path.Join(state.dir, file)); err != nil { - t.Fatalf("Failed to delete %s: %s", file, err.Error()) - } - } -} - -func TestCustomProceduresFull(t *testing.T) { - generateScripts(t, []string{}) - state.procs = AgentProcedures{ - Deps: Entrypoint{ - Name: pkg.scriptInstallDeps, - }, - Install: Entrypoint{ - Name: pkg.scriptInstallIofog, - Args: []string{ - "", - "", - "", - }, - }, - Uninstall: Entrypoint{ - Name: pkg.scriptUninstallIofog, - }, - } - runTest(t, state) -} - -func TestCustomProceduresPartial(t *testing.T) { - generateScripts(t, []string{pkg.scriptInstallIofog}) - state.procs = AgentProcedures{ - Deps: Entrypoint{ - Name: pkg.scriptInstallDeps, - }, - Uninstall: Entrypoint{ - Name: pkg.scriptUninstallIofog, - }, - } - runTest(t, state) - - // generateScripts(t, []string{pkg.scriptInstallIofog, pkg.scriptInstallDeps, pkg.scriptInstallContainerEngine, pkg.scriptInstallJava}) - // state.procs = AgentProcedures{ - // Uninstall: Entrypoint{ - // Name: pkg.scriptUninstallIofog, - // }, - // } - // runTest(t, state) - // - // generateScripts(t, []string{pkg.scriptUninstallIofog, pkg.scriptInstallIofog, pkg.scriptInstallDeps, pkg.scriptInstallContainerEngine, pkg.scriptInstallJava}) - // state.procs = AgentProcedures{} - // runTest(t, state) - // - // generateScripts(t, []string{pkg.scriptUninstallIofog, pkg.scriptInstallDeps, pkg.scriptInstallContainerEngine, pkg.scriptInstallJava}) - // state.procs = AgentProcedures{ - // Install: Entrypoint{ - // Name: pkg.scriptInstallIofog, - // Args: []string{ - // "", - // "", - // "", - // }, - // }, - // } - // runTest(t, state) -} diff --git a/pkg/iofog/install/test_helpers_test.go b/pkg/iofog/install/test_helpers_test.go new file mode 100644 index 000000000..7b7c56d9f --- /dev/null +++ b/pkg/iofog/install/test_helpers_test.go @@ -0,0 +1,38 @@ +package install + +import ( + "os" + "path" +) + +func copyDir(src, dst string) error { + if err := os.MkdirAll(dst, 0o755); err != nil { + return err + } + files, err := os.ReadDir(src) + if err != nil { + return err + } + for _, file := range files { + srcPath := path.Join(src, file.Name()) + dstPath := path.Join(dst, file.Name()) + if file.IsDir() { + if err := copyDir(srcPath, dstPath); err != nil { + return err + } + continue + } + if err := copyFile(srcPath, dstPath); err != nil { + return err + } + } + return nil +} + +func copyFile(src, dst string) error { + input, err := os.ReadFile(src) + if err != nil { + return err + } + return os.WriteFile(dst, input, 0o644) +} diff --git a/pkg/iofog/install/types.go b/pkg/iofog/install/types.go index f0bfdba91..b8c37998e 100644 --- a/pkg/iofog/install/types.go +++ b/pkg/iofog/install/types.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package install import ( @@ -26,15 +13,7 @@ type IofogUser struct { RefreshToken string } -type Auth struct { - URL string - Realm string - SSL string - RealmKey string - ControllerClient string - ControllerSecret string - ViewerClient string -} +type Auth = cpv3.Auth type Database struct { Provider string @@ -89,24 +68,35 @@ type VaultGoogleConfig struct { Credentials string } +type RemoteSystemImages struct { + ARM string `yaml:"arm,omitempty"` + AMD64 string `yaml:"amd64,omitempty"` + ARM64 string `yaml:"arm64,omitempty"` + RISCV64 string `yaml:"riscv64,omitempty"` +} + +type RemoteSystemMicroservices struct { + Router RemoteSystemImages `yaml:"router,omitempty"` + Nats RemoteSystemImages `yaml:"nats,omitempty"` +} + +type SiteCertificate struct { + TLSCert string + TLSKey string +} + type Pod struct { Name string Status string } type K8SControllerConfig struct { - // User IofogUser - Replicas int32 - ReplicasNats int32 - Database Database - PidBaseDir string - EcnViewerPort int - EcnViewerURL string - LogLevel string - Auth Auth - Events Events - Https *bool - SecretName string - Nats *cpv3.Nats - Vault *cpv3.Vault + Replicas int32 + ReplicasNats int32 + Database Database + Auth Auth + Events Events + Controller cpv3.Controller + Nats *cpv3.Nats + Vault *cpv3.Vault } diff --git a/pkg/iofog/install/verbosity.go b/pkg/iofog/install/verbosity.go index c669adfa5..c06a1c14c 100644 --- a/pkg/iofog/install/verbosity.go +++ b/pkg/iofog/install/verbosity.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package install import ( diff --git a/pkg/util/assets.go b/pkg/util/assets.go index 00b10b211..9a6238131 100644 --- a/pkg/util/assets.go +++ b/pkg/util/assets.go @@ -1,44 +1,15 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( "fmt" - "sync" - rice "github.com/GeertJohan/go.rice" + cliassets "github.com/eclipse-iofog/iofogctl/assets" ) -var assets *rice.Box - -var once sync.Once - func GetStaticFile(filename string) (string, error) { - var err error - once.Do(func() { - assets, err = rice.FindBox("../../assets") - }) - if err != nil { - msg := "could not initialize assets: %s" - err = fmt.Errorf(msg, err.Error()) - return "", err - } - fileContent, err := assets.String(filename) + fileContent, err := cliassets.FS.ReadFile(filename) if err != nil { - msg := "could not load static file %s: %s" - err = fmt.Errorf(msg, filename, err.Error()) - return "", err + return "", fmt.Errorf("could not load static file %s: %s", filename, err.Error()) } - return fileContent, nil + return string(fileContent), nil } diff --git a/pkg/util/debug.go b/pkg/util/debug.go index 1c140e5e0..d8c32eac2 100644 --- a/pkg/util/debug.go +++ b/pkg/util/debug.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util var debug bool = false diff --git a/pkg/util/edgelet_binary.go b/pkg/util/edgelet_binary.go new file mode 100644 index 000000000..0b2d9ead4 --- /dev/null +++ b/pkg/util/edgelet_binary.go @@ -0,0 +1,191 @@ +package util + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" +) + +const edgeletDownloadTimeout = 10 * time.Minute + +var edgeletHTTPClient = &http.Client{Timeout: edgeletDownloadTimeout} + +var supportedEdgeletArches = map[string]struct{}{ + "amd64": {}, + "arm64": {}, + "arm": {}, + "riscv64": {}, +} + +// NormalizeEdgeletOS maps host GOOS/uname values to edgelet release OS names. +func NormalizeEdgeletOS(osName string) (string, error) { + switch strings.ToLower(strings.TrimSpace(osName)) { + case "linux": + return "linux", nil + case "darwin", "macos", "osx": + return "darwin", nil + case "windows", "win32": + return "windows", nil + default: + return "", fmt.Errorf("unsupported edgelet OS %q", osName) + } +} + +// NormalizeEdgeletArch maps SDK arch strings to edgelet release arch suffixes. +func NormalizeEdgeletArch(archName string) (string, error) { + archName = strings.ToLower(strings.TrimSpace(archName)) + if archName == "auto" { + return "auto", nil + } + if _, ok := supportedEdgeletArches[archName]; !ok { + return "", fmt.Errorf("unsupported edgelet arch %q", archName) + } + return archName, nil +} + +// EdgeletBinaryArtifact returns the release artifact filename for os/arch. +func EdgeletBinaryArtifact(osName, archName string) (string, error) { + osName, err := NormalizeEdgeletOS(osName) + if err != nil { + return "", err + } + archName, err = NormalizeEdgeletArch(archName) + if err != nil { + return "", err + } + if archName == "auto" { + return "", fmt.Errorf("edgelet arch must be resolved before building artifact name") + } + if osName == "windows" { + if archName != "amd64" { + return "", fmt.Errorf("windows edgelet supports amd64 only, got %q", archName) + } + return "edgelet-windows-amd64.exe", nil + } + return fmt.Sprintf("edgelet-%s-%s", osName, archName), nil +} + +// EdgeletBinaryURL builds the GitHub release download URL for a platform binary. +func EdgeletBinaryURL(osName, archName string) (string, error) { + artifact, err := EdgeletBinaryArtifact(osName, archName) + if err != nil { + return "", err + } + base := strings.TrimRight(GetEdgeletReleaseBase(), "/") + version := GetEdgeletBinaryVersion() + if base == "" || base == "undefined" { + return "", fmt.Errorf("edgelet release base is not configured") + } + if version == "" || version == "undefined" { + return "", fmt.Errorf("edgelet binary version is not configured") + } + return fmt.Sprintf("%s/%s/%s", base, version, artifact), nil +} + +// EdgeletChecksumsURL returns the SHA256SUMS manifest URL for the pinned release. +func EdgeletChecksumsURL() (string, error) { + base := strings.TrimRight(GetEdgeletReleaseBase(), "/") + version := GetEdgeletBinaryVersion() + if base == "" || base == "undefined" { + return "", fmt.Errorf("edgelet release base is not configured") + } + if version == "" || version == "undefined" { + return "", fmt.Errorf("edgelet binary version is not configured") + } + return fmt.Sprintf("%s/%s/SHA256SUMS", base, version), nil +} + +// ShouldSkipInstallDeps reports whether the deps layer is a no-op for the runtime config. +func ShouldSkipInstallDeps(containerEngine, deploymentType string) bool { + engine := strings.ToLower(strings.TrimSpace(containerEngine)) + if engine == "" || engine == "edgelet" { + return true + } + _ = deploymentType + return false +} + +func validateEdgeletDownloadURL(downloadURL string) error { + download, err := url.Parse(downloadURL) + if err != nil { + return fmt.Errorf("parse download URL: %w", err) + } + if download.Host == "" { + return fmt.Errorf("download URL missing host") + } + + base, err := url.Parse(strings.TrimRight(GetEdgeletReleaseBase(), "/")) + if err != nil { + return fmt.Errorf("parse edgelet release base: %w", err) + } + if base.Host == "" { + return fmt.Errorf("edgelet release base missing host") + } + if download.Host != base.Host { + return fmt.Errorf("unexpected download host %q", download.Host) + } + if base.Scheme == "https" && download.Scheme != "https" { + return fmt.Errorf("download URL must use HTTPS") + } + if download.Scheme != "http" && download.Scheme != "https" { + return fmt.Errorf("unsupported download scheme %q", download.Scheme) + } + + version := GetEdgeletBinaryVersion() + wantPrefix := strings.TrimRight(base.Path, "/") + "/" + version + "/" + if !strings.HasPrefix(download.Path, wantPrefix) { + return fmt.Errorf("unexpected download path %q", download.Path) + } + return nil +} + +// DownloadEdgeletBinary fetches the release binary for os/arch into destPath. +func DownloadEdgeletBinary(osName, archName, destPath string) error { + return downloadEdgeletBinary(context.Background(), osName, archName, destPath, edgeletHTTPClient) +} + +func downloadEdgeletBinary(ctx context.Context, osName, archName, destPath string, client *http.Client) error { + downloadURL, err := EdgeletBinaryURL(osName, archName) + if err != nil { + return err + } + if err := validateEdgeletDownloadURL(downloadURL); err != nil { + return fmt.Errorf("validate edgelet download URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil) + if err != nil { + return fmt.Errorf("create edgelet download request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("download edgelet binary: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download edgelet binary: HTTP %d from %s", resp.StatusCode, downloadURL) + } + + if err := os.MkdirAll(filepath.Dir(destPath), DirPerm); err != nil { + return fmt.Errorf("create download dir: %w", err) + } + + out, err := CreateUserFile(destPath, ExecPerm) // #nosec G302 -- executable edgelet binary + if err != nil { + return fmt.Errorf("create edgelet binary file: %w", err) + } + defer IgnoreClose(out) + + if _, err := io.Copy(out, resp.Body); err != nil { + return fmt.Errorf("write edgelet binary: %w", err) + } + return nil +} diff --git a/pkg/util/edgelet_binary_test.go b/pkg/util/edgelet_binary_test.go new file mode 100644 index 000000000..54121ed08 --- /dev/null +++ b/pkg/util/edgelet_binary_test.go @@ -0,0 +1,139 @@ +package util + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" +) + +func TestEdgeletBinaryArtifact(t *testing.T) { + tests := []struct { + os string + arch string + want string + wantErr bool + }{ + {"linux", "amd64", "edgelet-linux-amd64", false}, + {"linux", "arm64", "edgelet-linux-arm64", false}, + {"linux", "arm", "edgelet-linux-arm", false}, + {"linux", "riscv64", "edgelet-linux-riscv64", false}, + {"darwin", "amd64", "edgelet-darwin-amd64", false}, + {"darwin", "arm64", "edgelet-darwin-arm64", false}, + {"windows", "amd64", "edgelet-windows-amd64.exe", false}, + {"windows", "arm64", "", true}, + {"freebsd", "amd64", "", true}, + {"linux", "auto", "", true}, + } + + for _, tt := range tests { + got, err := EdgeletBinaryArtifact(tt.os, tt.arch) + if tt.wantErr { + if err == nil { + t.Fatalf("EdgeletBinaryArtifact(%q, %q) expected error", tt.os, tt.arch) + } + continue + } + if err != nil { + t.Fatalf("EdgeletBinaryArtifact(%q, %q): %v", tt.os, tt.arch, err) + } + if got != tt.want { + t.Fatalf("EdgeletBinaryArtifact(%q, %q) = %q, want %q", tt.os, tt.arch, got, tt.want) + } + } +} + +func TestEdgeletBinaryURL(t *testing.T) { + SetEdgeletReleaseBaseForTest("https://github.com/Datasance/edgelet/releases/download") + SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") + t.Cleanup(ResetEdgeletReleaseBaseForTest) + t.Cleanup(ResetEdgeletBinaryVersionForTest) + + got, err := EdgeletBinaryURL("linux", "amd64") + if err != nil { + t.Fatalf("EdgeletBinaryURL: %v", err) + } + want := "https://github.com/Datasance/edgelet/releases/download/v1.0.0-rc.6/edgelet-linux-amd64" + if got != want { + t.Fatalf("EdgeletBinaryURL = %q, want %q", got, want) + } + if err := validateEdgeletDownloadURL(got); err != nil { + t.Fatalf("validateEdgeletDownloadURL(%q): %v", got, err) + } +} + +func TestShouldSkipInstallDeps(t *testing.T) { + tests := []struct { + engine string + deploy string + expected bool + }{ + {"edgelet", "native", true}, + {"edgelet", "container", true}, + {"", "native", true}, + {"docker", "native", false}, + {"podman", "container", false}, + } + + for _, tt := range tests { + if got := ShouldSkipInstallDeps(tt.engine, tt.deploy); got != tt.expected { + t.Fatalf("ShouldSkipInstallDeps(%q, %q) = %v, want %v", tt.engine, tt.deploy, got, tt.expected) + } + } +} + +func TestDownloadEdgeletBinaryGitHubReleasePath(t *testing.T) { + const releasePath = "/Datasance/edgelet/releases/download/v1.0.0-rc.6/edgelet-linux-arm64" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != releasePath { + http.NotFound(w, r) + return + } + _, _ = w.Write([]byte("edgelet-binary-bytes")) + })) + defer server.Close() + + SetEdgeletReleaseBaseForTest(server.URL + "/Datasance/edgelet/releases/download") + SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") + t.Cleanup(ResetEdgeletReleaseBaseForTest) + t.Cleanup(ResetEdgeletBinaryVersionForTest) + + dir := t.TempDir() + dest := filepath.Join(dir, "edgelet-linux-arm64") + + if err := DownloadEdgeletBinary("linux", "arm64", dest); err != nil { + t.Fatalf("DownloadEdgeletBinary: %v", err) + } +} + +func TestDownloadEdgeletBinary(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1.0.0-rc.6/edgelet-linux-amd64" { + http.NotFound(w, r) + return + } + _, _ = w.Write([]byte("edgelet-binary-bytes")) + })) + defer server.Close() + + SetEdgeletReleaseBaseForTest(server.URL) + SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") + t.Cleanup(ResetEdgeletReleaseBaseForTest) + t.Cleanup(ResetEdgeletBinaryVersionForTest) + + dir := t.TempDir() + dest := filepath.Join(dir, "edgelet-linux-amd64") + + if err := DownloadEdgeletBinary("linux", "amd64", dest); err != nil { + t.Fatalf("DownloadEdgeletBinary: %v", err) + } + + data, err := os.ReadFile(dest) + if err != nil { + t.Fatalf("read downloaded binary: %v", err) + } + if string(data) != "edgelet-binary-bytes" { + t.Fatalf("unexpected binary contents: %q", string(data)) + } +} diff --git a/pkg/util/edgelet_binary_testutil.go b/pkg/util/edgelet_binary_testutil.go new file mode 100644 index 000000000..25e6e4525 --- /dev/null +++ b/pkg/util/edgelet_binary_testutil.go @@ -0,0 +1,21 @@ +package util + +// SetEdgeletReleaseBaseForTest overrides the ldflag release base in unit tests. +func SetEdgeletReleaseBaseForTest(base string) { + edgeletReleaseBase = base +} + +// SetEdgeletBinaryVersionForTest overrides the ldflag binary version in unit tests. +func SetEdgeletBinaryVersionForTest(version string) { + edgeletBinaryVersion = version +} + +// ResetEdgeletReleaseBaseForTest restores the ldflag release base after tests. +func ResetEdgeletReleaseBaseForTest() { + edgeletReleaseBase = "undefined" +} + +// ResetEdgeletBinaryVersionForTest restores the ldflag binary version after tests. +func ResetEdgeletBinaryVersionForTest() { + edgeletBinaryVersion = "undefined" +} diff --git a/pkg/util/errors.go b/pkg/util/errors.go index 5c97e7f78..c9e850bfc 100644 --- a/pkg/util/errors.go +++ b/pkg/util/errors.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( @@ -120,7 +107,7 @@ func NewInternalError(message string) *InternalError { // Error export func (err *InternalError) Error() string { - return "Unexpected internal behaviour\n" + err.message + return "Unexpected internal behavior\n" + err.message } // HTTPError export diff --git a/pkg/util/exec.go b/pkg/util/exec.go index b1fae0c8a..48604414b 100644 --- a/pkg/util/exec.go +++ b/pkg/util/exec.go @@ -1,28 +1,21 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( "bytes" + "fmt" "os" "os/exec" + "strings" ) // Exec command func Exec(env, cmdName string, args ...string) (stdout bytes.Buffer, err error) { - // Instantiate command object - cmd := exec.Command(cmdName, args...) + if IsDebug() { + fmt.Printf("[LOCAL]: Running: %s %s\n", cmdName, strings.Join(args, " ")) + } + + // Instantiate command object — callers pass fixed deploy commands (sh, sudo, kubectl). + cmd := exec.Command(cmdName, args...) // #nosec G204 // Instantiate output objects var stderr bytes.Buffer @@ -36,8 +29,14 @@ func Exec(env, cmdName string, args ...string) (stdout bytes.Buffer, err error) // Run command err = cmd.Run() if err != nil { + if IsDebug() && stderr.Len() > 0 { + fmt.Printf("[LOCAL]: stderr: %s\n", strings.TrimSpace(stderr.String())) + } err = NewInternalError(stderr.String()) return } + if IsDebug() && stdout.Len() > 0 { + fmt.Printf("[LOCAL]: stdout: %s\n", strings.TrimSpace(stdout.String())) + } return } diff --git a/pkg/util/get_controller_endpoint.go b/pkg/util/get_controller_endpoint.go index 0201476e7..15cf8fbd1 100644 --- a/pkg/util/get_controller_endpoint.go +++ b/pkg/util/get_controller_endpoint.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( diff --git a/pkg/util/get_controller_endpoint_test.go b/pkg/util/get_controller_endpoint_test.go index 8af28deeb..9bdc90079 100644 --- a/pkg/util/get_controller_endpoint_test.go +++ b/pkg/util/get_controller_endpoint_test.go @@ -1,15 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2020 Red Hat, Inc. - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ package util import ( diff --git a/pkg/util/iohelpers.go b/pkg/util/iohelpers.go new file mode 100644 index 000000000..74cd249a1 --- /dev/null +++ b/pkg/util/iohelpers.go @@ -0,0 +1,43 @@ +package util + +import ( + "io" + "os" +) + +// IgnoreErr discards an error (interactive I/O cleanup paths). +func IgnoreErr(err error) {} + +// IgnoreClose closes c and discards errors. +func IgnoreClose(c io.Closer) { + if c != nil { + _ = c.Close() + } +} + +// WriteStdout writes data to stdout and syncs when possible. +func WriteStdout(data []byte) { + if _, err := os.Stdout.Write(data); err != nil { + return + } + _ = os.Stdout.Sync() +} + +// WriteStdoutString writes s to stdout and syncs when possible. +func WriteStdoutString(s string) { + WriteStdout([]byte(s)) +} + +// WriteStderrString writes s to stderr. +func WriteStderrString(s string) { + _, _ = os.Stderr.WriteString(s) +} + +// DrainAndCloseHTTPBody drains and closes an HTTP response body. +func DrainAndCloseHTTPBody(body io.ReadCloser) { + if body == nil { + return + } + _, _ = io.Copy(io.Discard, body) + _ = body.Close() +} diff --git a/pkg/util/is_local.go b/pkg/util/is_local.go index 6d551d6e9..da6673b30 100644 --- a/pkg/util/is_local.go +++ b/pkg/util/is_local.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( diff --git a/pkg/util/is_system_msvc.go b/pkg/util/is_system_msvc.go index 5df670d57..363c54ce1 100644 --- a/pkg/util/is_system_msvc.go +++ b/pkg/util/is_system_msvc.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( diff --git a/pkg/util/net.go b/pkg/util/net.go new file mode 100644 index 000000000..b2bea03d7 --- /dev/null +++ b/pkg/util/net.go @@ -0,0 +1,50 @@ +package util + +import ( + "fmt" + "net" + "strconv" + "time" +) + +const defaultDialTimeout = 500 * time.Millisecond + +// IsTCPPortOpen reports whether something is accepting connections on host:port. +func IsTCPPortOpen(host string, port int) bool { + address := net.JoinHostPort(host, strconv.Itoa(port)) + conn, err := net.DialTimeout("tcp", address, defaultDialTimeout) + if err != nil { + return false + } + _ = conn.Close() + return true +} + +// DetectLocalHostIPv4 returns the first non-loopback IPv4 address on a local interface. +func DetectLocalHostIPv4() (string, error) { + ifaces, err := net.Interfaces() + if err != nil { + return "", err + } + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok || ipNet.IP.IsLoopback() { + continue + } + ip4 := ipNet.IP.To4() + if ip4 == nil { + continue + } + return ip4.String(), nil + } + } + return "", fmt.Errorf("no non-loopback IPv4 address found on local interfaces") +} diff --git a/pkg/util/net_test.go b/pkg/util/net_test.go new file mode 100644 index 000000000..e10823e1a --- /dev/null +++ b/pkg/util/net_test.go @@ -0,0 +1,33 @@ +package util + +import ( + "net" + "testing" +) + +func TestIsTCPPortOpen(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + defer ln.Close() + + port := ln.Addr().(*net.TCPAddr).Port + if !IsTCPPortOpen("127.0.0.1", port) { + t.Fatalf("expected port %d to be open", port) + } + if IsTCPPortOpen("127.0.0.1", 1) { + t.Fatal("did not expect port 1 to be open") + } +} + +func TestDetectLocalHostIPv4(t *testing.T) { + ip, err := DetectLocalHostIPv4() + if err != nil { + t.Skipf("DetectLocalHostIPv4 unavailable in this environment: %v", err) + } + parsed := net.ParseIP(ip) + if parsed == nil || parsed.To4() == nil { + t.Fatalf("expected IPv4 address, got %q", ip) + } +} diff --git a/pkg/util/permissions.go b/pkg/util/permissions.go new file mode 100644 index 000000000..af9b7e9f2 --- /dev/null +++ b/pkg/util/permissions.go @@ -0,0 +1,10 @@ +package util + +const ( + // DirPerm is the default mode for CLI config and cache directories. + DirPerm = 0o700 + // FilePerm is the default mode for CLI config and cache files. + FilePerm = 0o600 + // ExecPerm is for downloaded binaries and materialized shell scripts that must run. + ExecPerm = 0o755 +) diff --git a/pkg/util/print.go b/pkg/util/print.go index ff5546276..228521595 100644 --- a/pkg/util/print.go +++ b/pkg/util/print.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( @@ -25,6 +12,7 @@ const CSkyblue = "\033[38;5;117m" const CDeepskyblue = "\033[48;5;25m" const Red = "\033[38;5;1m" const Green = "\033[38;5;28m" +const Yellow = "\033[38;5;220m" var progressPrintMu sync.Mutex @@ -32,7 +20,7 @@ var progressPrintMu sync.Mutex func PrintInfo(message string) { wasRunning := SpinPause() message = FirstToUpper(message) - fmt.Printf(CSkyblue + message + NoFormat + "\n") + fmt.Print(CSkyblue + message + NoFormat + "\n") if wasRunning { SpinUnpause() } @@ -42,7 +30,7 @@ func PrintInfo(message string) { func PrintNotify(message string) { wasRunning := SpinPause() message = FirstToUpper(message) - fmt.Fprintf(os.Stderr, CSkyblue+"! "+message+NoFormat+"\n") + fmt.Fprintf(os.Stderr, "%s", CSkyblue+"! "+message+NoFormat+"\n") if wasRunning { SpinUnpause() } @@ -69,12 +57,22 @@ func PrintProgress(label string, percent int, done bool) { func PrintSuccess(message string) { SpinStop() message = FirstToUpper(message) - fmt.Printf(Green + "✔ " + message + NoFormat + "\n") + fmt.Print(Green + "✔ " + message + NoFormat + "\n") } // Print 'message' with red color text func PrintError(message string) { SpinStop() message = FirstToUpper(message) - fmt.Fprintf(os.Stderr, Red+"✘ "+message+NoFormat+"\n") + fmt.Fprintf(os.Stderr, "%s", Red+"✘ "+message+NoFormat+"\n") +} + +// PrintWarning prints a yellow warning to stderr. +func PrintWarning(message string) { + wasRunning := SpinPause() + message = FirstToUpper(message) + fmt.Fprintf(os.Stderr, "%s", Yellow+"! "+message+NoFormat+"\n") + if wasRunning { + SpinUnpause() + } } diff --git a/pkg/util/rand.go b/pkg/util/rand.go deleted file mode 100644 index c69f123ec..000000000 --- a/pkg/util/rand.go +++ /dev/null @@ -1,35 +0,0 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - -package util - -import ( - "math/rand" - "time" -) - -func init() { - rand.Seed(time.Now().UTC().UnixNano()) -} - -func RandomString(size int, chars string) string { - buf := make([]byte, size) - for idx := range buf { - buf[idx] = chars[rand.Intn(len(chars))] - } - return string(buf) -} - -const AlphaNum = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" -const Alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" -const AlphaLower = "abcdefghijklmnopqrstuvwxyz" diff --git a/pkg/util/rice-box.go b/pkg/util/rice-box.go deleted file mode 100644 index 32088f31e..000000000 --- a/pkg/util/rice-box.go +++ /dev/null @@ -1,420 +0,0 @@ -// Code generated by rice embed-go; DO NOT EDIT. -package util - -import ( - "time" - - "github.com/GeertJohan/go.rice/embedded" -) - -func init() { - - // define files - file3 := &embedded.EmbeddedFile{ - Filename: "agent/check_prereqs.sh", - FileModTime: time.Unix(1775031333, 0), - - Content: string("#!/bin/sh\nset -x\n\n# Check can sudo without password\nif ! $(sudo ls /tmp/ > /dev/null); then\n\tMSG=\"Unable to successfully use sudo with user $USER on this host.\\nUser $USER must be in sudoers group and using sudo without password must be enabled.\\nPlease see iofog.org documentation for more details.\"\n\techo $MSG\n\texit 1\nfi"), - } - file4 := &embedded.EmbeddedFile{ - Filename: "agent/init.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\n# Script to detect Linux distribution and version\n# Used as a precursor for system-specific installations\n\n# Exit on error and print commands for debugging\nset -e\nset -x\n\n# Define user variable\nuser=\"$(id -un 2>/dev/null || true)\"\n\n# Check if a command exists\ncommand_exists() {\n command -v \"$@\" > /dev/null 2>&1\n}\n\n# Detect the Linux distribution\nget_distribution() {\n lsb_dist=\"\"\n dist_version=\"\"\n \n # Every system that we officially support has /etc/os-release\n if [ -r /etc/os-release ]; then\n \n lsb_dist=\"$(. /etc/os-release && echo \"$ID\")\"\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n lsb_dist=\"$(echo \"$lsb_dist\" | tr '[:upper:]' '[:lower:]')\"\n else\n echo \"Error: Unsupported Linux distribution! /etc/os-release not found.\"\n exit 1\n fi\n \n echo \"# Detected distribution: $lsb_dist (version: $dist_version)\"\n}\n\n# Check if this is a forked Linux distro\ncheck_forked() {\n # Skip if lsb_release doesn't exist\n if ! command_exists lsb_release; then\n return\n fi\n \n # Check if the `-u` option is supported\n set +e\n lsb_release -a > /dev/null 2>&1\n lsb_release_exit_code=$?\n set -e\n\n # Check if the command has exited successfully, it means we're in a forked distro\n if [ \"$lsb_release_exit_code\" = \"0\" ]; then\n # Get the upstream release info\n current_lsb_dist=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]')\n current_dist_version=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]')\n\n # Print info about current distro\n echo \"You're using '$current_lsb_dist' version '$current_dist_version'.\"\n \n # Check if current is different from detected (indicating a fork)\n if [ \"$current_lsb_dist\" != \"$lsb_dist\" ] || [ \"$current_dist_version\" != \"$dist_version\" ]; then\n echo \"Upstream release is '$lsb_dist' version '$dist_version'.\"\n fi\n else\n # Additional checks for specific distros that might not be properly detected\n if [ -r /etc/debian_version ] && [ \"$lsb_dist\" != \"ubuntu\" ] && [ \"$lsb_dist\" != \"raspbian\" ]; then\n if [ \"$lsb_dist\" = \"osmc\" ]; then\n # OSMC runs Raspbian\n lsb_dist=raspbian\n else\n # We're Debian and don't even know it!\n lsb_dist=debian\n fi\n # Get Debian version and map it to codename\n dist_version=\"$(sed 's/\\/.*//' /etc/debian_version | sed 's/\\..*//')\"\n case \"$dist_version\" in\n 14)\n dist_version=\"forky\"\n ;;\n 13)\n dist_version=\"trixie\"\n ;;\n 12)\n dist_version=\"bookworm\"\n ;;\n 11)\n dist_version=\"bullseye\"\n ;;\n 10)\n dist_version=\"buster\"\n ;;\n 9)\n dist_version=\"stretch\"\n ;;\n 8|'Kali Linux 2')\n dist_version=\"jessie\"\n ;;\n 7)\n dist_version=\"wheezy\"\n ;;\n esac\n elif [ -r /etc/redhat-release ] && [ -z \"$lsb_dist\" ]; then\n lsb_dist=redhat\n # Extract version from redhat-release file\n dist_version=\"$(sed 's/.*release \\([0-9.]*\\).*/\\1/' /etc/redhat-release)\"\n fi\n fi\n}\n\n# Set up sudo command if necessary\nsetup_sudo() {\n sh_c='sh -c'\n if [ \"$user\" != 'root' ]; then\n if command_exists sudo; then\n sh_c='sudo -E sh -c'\n elif command_exists su; then\n sh_c='su -c'\n else\n echo \"Error: this installer needs the ability to run commands as root.\"\n echo \"We are unable to find either 'sudo' or 'su' available to make this happen.\"\n exit 1\n fi\n fi\n echo \"# Using command executor: $sh_c\"\n}\n\n# Refine distribution version detection based on the distro\nrefine_distribution_version() {\n case \"$lsb_dist\" in\n ubuntu)\n if command_exists lsb_release; then\n dist_version=\"$(lsb_release --codename | cut -f2)\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/lsb-release ]; then\n \n dist_version=\"$(. /etc/lsb-release && echo \"$DISTRIB_CODENAME\")\"\n fi\n ;;\n\n debian|raspbian)\n # If we only have a number, map it to a codename for better recognition\n if echo \"$dist_version\" | grep -qE '^[0-9]+$'; then\n case \"$dist_version\" in\n 14)\n dist_version=\"forky\"\n ;;\n 13)\n dist_version=\"trixie\"\n ;;\n 12)\n dist_version=\"bookworm\"\n ;;\n 11)\n dist_version=\"bullseye\"\n ;;\n 10)\n # Handle special case for Buster\n dist_version=\"buster\"\n if [ \"$user\" = 'root' ]; then\n apt-get update --allow-releaseinfo-change || true\n elif command_exists sudo; then\n sudo apt-get update --allow-releaseinfo-change || true\n fi\n ;;\n 9)\n dist_version=\"stretch\"\n ;;\n 8)\n dist_version=\"jessie\"\n ;;\n 7)\n dist_version=\"wheezy\"\n ;;\n esac\n fi\n ;;\n\n centos|rhel|fedora|ol)\n # Make sure we have a version number\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/redhat-release ]; then\n dist_version=\"$(sed 's/.*release \\([0-9.]*\\).*/\\1/' /etc/redhat-release)\"\n fi\n ;;\n\n sles|opensuse)\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n # Fallback for older versions\n if [ -z \"$dist_version\" ] && [ -r /etc/SuSE-release ]; then\n dist_version=\"$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = //')\"\n fi\n # Ensure version is in the correct format (e.g., 15.4 for SLES 15 SP4)\n if [ -n \"$dist_version\" ]; then\n # Remove any non-numeric characters except dots\n dist_version=\"$(echo \"$dist_version\" | sed 's/[^0-9.]//g')\"\n fi\n # Normalize distribution name\n if [ \"$lsb_dist\" = \"sles\" ]; then\n lsb_dist=\"sles\"\n elif [ \"$lsb_dist\" = \"opensuse\" ]; then\n lsb_dist=\"opensuse\"\n fi\n ;;\n\n *)\n if command_exists lsb_release; then\n dist_version=\"$(lsb_release --release | cut -f2)\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n ;;\n esac\n}\n\n# Detect init system \ndetect_init_system() {\n if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then\n INIT_SYSTEM=\"systemd\"\n elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then\n INIT_SYSTEM=\"upstart\"\n elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then\n INIT_SYSTEM=\"openrc\"\n elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then\n INIT_SYSTEM=\"s6\"\n elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then\n INIT_SYSTEM=\"runit\"\n elif [ -d /etc/init.d ]; then\n INIT_SYSTEM=\"sysvinit\"\n else\n INIT_SYSTEM=\"unknown\"\n fi\n export INIT_SYSTEM\n echo \"# Detected init system: $INIT_SYSTEM\"\n}\n\n# Detect package type (deb, rpm, or other)\ndetect_package_type() {\n case \"$lsb_dist\" in\n debian|ubuntu|raspbian|mendel)\n PACKAGE_TYPE=\"deb\"\n ;;\n fedora|centos|rhel|ol|sles|opensuse*)\n PACKAGE_TYPE=\"rpm\"\n ;;\n alpine)\n PACKAGE_TYPE=\"apk\"\n ;;\n *)\n if command_exists apt-get || command_exists dpkg; then\n PACKAGE_TYPE=\"deb\"\n elif command_exists yum || command_exists dnf || command_exists zypper; then\n PACKAGE_TYPE=\"rpm\"\n elif command_exists apk; then\n PACKAGE_TYPE=\"apk\"\n else\n PACKAGE_TYPE=\"other\"\n fi\n ;;\n esac\n export PACKAGE_TYPE\n echo \"# Detected package type: $PACKAGE_TYPE\"\n}\n\n# Init function\ninit() {\n # Detect basic distribution info\n get_distribution\n \n # Set up sudo for privileged commands\n setup_sudo\n \n # Refine version information\n refine_distribution_version\n \n # Check if this is a forked distro\n check_forked\n \n # Detect init system and package type (for universal OS/init support)\n detect_init_system\n detect_package_type\n \n # Print final distribution information\n echo \"----------------------------------------\"\n echo \"Linux Distribution: $lsb_dist\"\n echo \"Version: $dist_version\"\n echo \"Init system: $INIT_SYSTEM\"\n echo \"Package type: $PACKAGE_TYPE\"\n echo \"----------------------------------------\"\n \n}\n"), - } - file5 := &embedded.EmbeddedFile{ - Filename: "agent/install_container_engine.sh", - FileModTime: time.Unix(1775031421, 0), - - Content: string("#!/bin/sh\n# Script to install Docker/Podman based on Linux distribution\n# Sources init.sh for distribution detection\n\nset -x\nset -e\n\nCONTAINER_ENGINE_MSG=\"This operating system does not support automatic container engine installation. Please install Docker 25+ or Podman 4+ on the target host and re-run, or use an airgap deployment with a pre-installed engine.\"\n\n# Check Docker version (need >= 25). Sets docker_version_num for comparison.\ncheck_docker_version() {\n docker_version_num=0\n if command -v docker >/dev/null 2>&1; then\n raw=$(docker -v 2>/dev/null | sed 's/.*version \\([^,]*\\),.*/\\1/' | tr -d '.')\n [ -n \"$raw\" ] && docker_version_num=\"$raw\"\n fi\n [ \"$docker_version_num\" -ge 2500 ] 2>/dev/null || return 1\n}\n\n# Check Podman version (need >= 4). Sets podman_version_num for comparison.\ncheck_podman_version() {\n podman_version_num=0\n if command -v podman >/dev/null 2>&1; then\n raw=$(podman --version 2>/dev/null | sed -n 's/.*version \\([0-9][0-9]*\\).*/\\1/p')\n [ -n \"$raw\" ] && podman_version_num=\"$raw\"\n fi\n [ \"$podman_version_num\" -ge 4 ] 2>/dev/null || return 1\n}\n\n# Start Docker daemon (init-aware)\nstart_docker() {\n set +e\n if $sh_c \"docker ps\" >/dev/null 2>&1; then\n set -e\n return 0\n fi\n err_code=1\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl start docker\" >/dev/null 2>&1\n err_code=$?\n ;;\n sysvinit)\n $sh_c \"service docker start\" >/dev/null 2>&1 || $sh_c \"/etc/init.d/docker start\" >/dev/null 2>&1\n err_code=$?\n ;;\n openrc)\n $sh_c \"rc-service docker start\" >/dev/null 2>&1\n err_code=$?\n ;;\n *)\n $sh_c \"/etc/init.d/docker start\" >/dev/null 2>&1\n err_code=$?\n [ $err_code -ne 0 ] && $sh_c \"systemctl start docker\" >/dev/null 2>&1 && err_code=0\n [ $err_code -ne 0 ] && $sh_c \"service docker start\" >/dev/null 2>&1 && err_code=0\n [ $err_code -ne 0 ] && $sh_c \"snap start docker\" >/dev/null 2>&1 && err_code=0\n ;;\n esac\n set -e\n if [ $err_code -ne 0 ]; then\n echo \"Could not start Docker daemon\"\n exit 1\n fi\n}\n\n# Start Podman (init-aware)\nstart_podman() {\n set +e\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl start podman\" >/dev/null 2>&1\n $sh_c \"systemctl start podman.socket\" >/dev/null 2>&1\n ;;\n sysvinit)\n $sh_c \"service podman start\" >/dev/null 2>&1 || $sh_c \"/etc/init.d/podman start\" >/dev/null 2>&1\n ;;\n openrc)\n $sh_c \"rc-service podman start\" >/dev/null 2>&1\n ;;\n *)\n $sh_c \"systemctl start podman\" >/dev/null 2>&1 || true\n $sh_c \"systemctl start podman.socket\" >/dev/null 2>&1 || true\n $sh_c \"service podman start\" >/dev/null 2>&1 || true\n ;;\n esac\n set -e\n}\n\n\ndo_modify_daemon() {\n # Skip for Podman installations\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"# Configuring Podman for CDI directory support...\"\n\n # Create CDI directories\n $sh_c \"mkdir -p /etc/cdi /var/run/cdi\"\n\n # Ensure /etc/containers exists\n $sh_c \"mkdir -p /etc/containers\"\n\n # Create containers.conf if it doesn't exist\n if [ ! -f \"/etc/containers/containers.conf\" ]; then\n $sh_c 'cat > /etc/containers/containers.conf <> /etc/containers/containers.conf'\n fi\n fi\n\n # Enable and start Podman (init-aware)\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl enable podman\" 2>/dev/null || true\n $sh_c \"systemctl enable podman.socket\" 2>/dev/null || true\n ;;\n openrc)\n $sh_c \"rc-update add podman default\" 2>/dev/null || true\n ;;\n sysvinit)\n $sh_c \"update-rc.d podman defaults\" 2>/dev/null || $sh_c \"chkconfig podman on\" 2>/dev/null || true\n ;;\n *) ;;\n esac\n start_podman\n return\n fi\n \n # Original Docker daemon configuration\n if [ ! -f /etc/docker/daemon.json ]; then\n echo \"Creating /etc/docker/daemon.json...\"\n $sh_c \"mkdir -p /etc/docker\"\n $sh_c 'cat > /etc/docker/daemon.json << EOF\n{\n\t\"storage-driver\": \"overlayfs\",\n \"features\": {\n \"containerd-snapshotter\": true,\n \"cdi\": true\n },\n \"cdi-spec-dirs\": [\"/etc/cdi/\", \"/var/run/cdi\"]\n}\nEOF'\n else\n echo \"/etc/docker/daemon.json already exists\"\n fi\n echo \"Restarting Docker daemon...\"\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl daemon-reload\"\n $sh_c \"systemctl restart docker\"\n ;;\n *)\n $sh_c \"systemctl daemon-reload\" 2>/dev/null || true\n $sh_c \"systemctl restart docker\" 2>/dev/null || start_docker\n ;;\n esac\n}\n\ndo_install_container_engine() {\n # PACKAGE_TYPE other: only check engine presence/version, do not install\n if [ \"$PACKAGE_TYPE\" = \"other\" ]; then\n if check_docker_version; then\n USE_PODMAN=\"false\"\n echo \"# Docker (>= 25) found; using Docker.\"\n start_docker\n do_modify_daemon\n return 0\n fi\n if check_podman_version; then\n USE_PODMAN=\"true\"\n echo \"# Podman (>= 4) found; using Podman.\"\n do_modify_daemon\n return 0\n fi\n echo \"Error: $CONTAINER_ENGINE_MSG\"\n exit 1\n fi\n\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"# Installing Podman and related packages...\"\n case \"$lsb_dist\" in\n fedora|centos|rhel|ol)\n $sh_c \"yum install -y podman crun podman-docker\"\n ;;\n sles|opensuse*)\n $sh_c \"zypper install -y podman crun podman-docker\"\n ;;\n esac\n if ! check_podman_version; then\n echo \"Error: Podman 4+ is required. Please upgrade Podman.\"\n exit 1\n fi\n do_modify_daemon\n return\n fi\n\n # Docker: check existing version first\n if command_exists docker; then\n docker_version=$(docker -v 2>/dev/null | sed 's/.*version \\([^,]*\\),.*/\\1/' | tr -d '.')\n if [ -n \"$docker_version\" ] && [ \"$docker_version\" -ge 2500 ] 2>/dev/null; then\n echo \"# Docker already installed (>= 25)\"\n start_docker\n do_modify_daemon\n return\n fi\n fi\n\n echo \"# Installing Docker...\"\n case \"$lsb_dist\" in\n debian|ubuntu|raspbian)\n case \"$dist_version\" in\n \"stretch\")\n $sh_c \"apt install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common\"\n curl -fsSL https://download.docker.com/linux/debian/gpg | $sh_c \"apt-key add -\"\n $sh_c \"add-apt-repository \\\"deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/debian $(lsb_release -cs) stable\\\"\"\n $sh_c \"apt update -y\"\n $sh_c \"apt install -y docker-ce\"\n ;;\n *)\n curl -fsSL https://get.docker.com/ | $sh_c \"sh\"\n ;;\n esac\n ;;\n *)\n curl -fsSL https://get.docker.com/ | $sh_c \"sh\"\n ;;\n esac\n\n if ! command_exists docker; then\n echo \"Failed to install Docker\"\n exit 1\n fi\n if ! check_docker_version; then\n echo \"Error: Docker 25+ is required. Please upgrade Docker.\"\n exit 1\n fi\n start_docker\n do_modify_daemon\n}\n\n# Check if we should use Podman based on distribution\ndetermine_container_engine() {\n USE_PODMAN=\"false\"\n case \"$lsb_dist\" in\n fedora|centos|rhel|ol|sles|opensuse*)\n USE_PODMAN=\"true\"\n echo \"# Using Podman for $lsb_dist\"\n ;;\n *)\n echo \"# Using Docker for $lsb_dist\"\n ;;\n esac\n}\n\n# Source init.sh to get distribution info\n. /etc/iofog/agent/init.sh\ninit\n\n# Configure container engine based on distribution\ndetermine_container_engine\n\n# Install appropriate container engine\ndo_install_container_engine\n\necho \"# Installation completed successfully\""), - } - file6 := &embedded.EmbeddedFile{ - Filename: "agent/install_deps.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\n/etc/iofog/agent/install_java.sh\n/etc/iofog/agent/install_container_engine.sh\n"), - } - file7 := &embedded.EmbeddedFile{ - Filename: "agent/install_iofog.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\ndo_check_install() {\n\tif command_exists iofog-agent; then\n\t\tlocal VERSION=$(sudo iofog-agent version | head -n1 | sed \"s/ioFog//g\" | tr -d ' ' | tr -d \"\\n\")\n\t\tif [ \"$VERSION\" = \"$agent_version\" ]; then\n\t\t\techo \"Agent $VERSION already installed.\"\n\t\t\texit 0\n\t\tfi\n\tfi\n}\n\ndo_stop_iofog() {\n\tif command_exists iofog-agent; then\n\t\tsudo systemctl stop iofog-agent\n\tfi\n}\n\n\n\ndo_install_iofog() {\n\tAGENT_CONFIG_FOLDER=/etc/iofog-agent\n\tSAVED_AGENT_CONFIG_FOLDER=/tmp/agent-config-save\n\techo \"# Installing ioFog agent...\"\n\n\t# Save iofog-agent config\n\tif [ -d ${AGENT_CONFIG_FOLDER} ]; then\n\t\tsudo rm -rf ${SAVED_AGENT_CONFIG_FOLDER}\n\t\tsudo mkdir -p ${SAVED_AGENT_CONFIG_FOLDER}\n\t\tsudo cp -r ${AGENT_CONFIG_FOLDER}/* ${SAVED_AGENT_CONFIG_FOLDER}/\n\tfi\n\n\techo $lsb_dist\n\tcase \"$lsb_dist\" in\n\t\tfedora|rhel|ol|centos)\n\t\t\t$sh_c \"yum update -y\"\n\t\t\t$sh_c \"yum install -y iofog-agent-$agent_version-1.noarch\"\n\t\t\t;;\n\t\tsles|opensuse)\n\t\t\t$sh_c \"zypper refresh\"\n\t\t\t$sh_c \"zypper install -y iofog-agent=$agent_version\"\n\t\t\t;;\n\t\t*)\n\t\t\t$sh_c \"apt update -qy\"\n\t\t\t$sh_c \"apt install --allow-downgrades iofog-agent=$agent_version -qy\"\n\t\t\t;;\n\tesac\n\n\t# Restore iofog-agent config\n\tif [ -d ${SAVED_AGENT_CONFIG_FOLDER} ]; then\n\t\tsudo mv ${SAVED_AGENT_CONFIG_FOLDER}/* ${AGENT_CONFIG_FOLDER}/\n\t\tsudo rmdir ${SAVED_AGENT_CONFIG_FOLDER}\n\tfi\n\tsudo chmod 775 ${AGENT_CONFIG_FOLDER}\n}\n\ndo_start_iofog(){\n\n\tsudo systemctl start iofog-agent > /dev/null 2&>1 &\n\tlocal STATUS=\"\"\n\tlocal ITER=0\n\twhile [ \"$STATUS\" != \"RUNNING\" ] ; do\n ITER=$((ITER+1))\n if [ \"$ITER\" -gt 600 ]; then\n echo 'Timed out waiting for Agent to be RUNNING'\n exit 1;\n fi\n sleep 1\n STATUS=$(sudo iofog-agent status | cut -f2 -d: | head -n 1 | tr -d '[:space:]')\n echo \"${STATUS}\"\n\tdone\n\tsudo iofog-agent \"config -cf 10 -sf 10\"\n\tif [ \"$lsb_dist\" = \"rhel\" ] || [ \"$lsb_dist\" = \"centos\" ] || [ \"$lsb_dist\" = \"fedora\" ] || [ \"$lsb_dist\" = \"ol\" ] || [ \"$lsb_dist\" = \"sles\" ] || [ \"$lsb_dist\" = \"opensuse\" ]; then\n sudo iofog-agent \"config -c unix:///var/run/podman/podman.sock\"\n fi \n}\n\nagent_version=\"$1\"\necho \"Using variables\"\necho \"version: $agent_version\"\n\n. /etc/iofog/agent/init.sh\ninit\n\n# Native agent is supported only on package-managed OSes (deb/rpm) with systemd\nif [ \"$PACKAGE_TYPE\" != \"deb\" ] && [ \"$PACKAGE_TYPE\" != \"rpm\" ]; then\n\techo \"Error: This operating system is not supported for native agent installation.\"\n\techo \"Please deploy the agent as a container (container agent) on this host.\"\n\texit 1\nfi\nif [ \"$INIT_SYSTEM\" != \"systemd\" ]; then\n\techo \"Error: Native agent is supported only on systemd. This system uses $INIT_SYSTEM.\"\n\techo \"Please deploy the agent as a container (container agent) on this host.\"\n\texit 1\nfi\n\ndo_check_install\ndo_stop_iofog\ndo_install_iofog\ndo_start_iofog"), - } - file8 := &embedded.EmbeddedFile{ - Filename: "agent/install_java.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\njava_major_version=0\njava_minor_version=0\ndo_check_install() {\n\tif command_exists java; then\n java_major_version=\"$(java --version | head -n1 | awk '{print $2}' | cut -d. -f1)\"\n java_minor_version=\"$(java --version | head -n1 | awk '{print $2}' | cut -d. -f2)\"\n\tfi\n\tif [ \"$java_major_version\" -ge \"17\" ] && [ \"$java_minor_version\" -ge \"0\" ]; then\n\t\techo \"Java $java_major_version.$java_minor_version already installed.\"\n\t\texit 0\n\tfi\n}\n\ndo_install_java() {\n\techo \"# Installing java 17...\"\n\techo \"\"\n\tos_arch=$(getconf LONG_BIT)\n\tis_arm=\"\"\n\tif [ \"$lsb_dist\" = \"raspbian\" ] || [ \"$(uname -m)\" = \"armv7l\" ] || [ \"$(uname -m)\" = \"aarch64\" ] || [ \"$(uname -m)\" = \"armv8\" ]; then\n\t\tis_arm=\"-arm\"\n\tfi\n\tcase \"$lsb_dist\" in\n\t\tubuntu|debian|raspbian|mendel)\n\t\t\t$sh_c \"apt-get update -y\"\n\t\t\t$sh_c \"apt install -y openjdk-17-jdk\"\n\t\t;;\n\t\tfedora|centos|rhel|ol)\n\t\t\t$sh_c \"yum install -y java-17-openjdk\"\n\t\t;;\n\t\tsles|opensuse*)\n\t\t\t$sh_c \"zypper refresh\"\n\t\t\t$sh_c \"zypper install -y java-17-openjdk\"\n\t\t;;\n\t\t*)\n\t\t\techo \"Unsupported distribution: $lsb_dist\"\n\t\t\texit 1\n\t\t;;\n\tesac\n}\n\ndo_install_deps() {\n\tlocal installer=\"\"\n\tcase \"$lsb_dist\" in\n\t\tubuntu|debian|raspbian|mendel)\n\t\t\tinstaller=\"apt\"\n\t\t;;\n\t\tfedora|centos|rhel|ol)\n\t\t\tinstaller=\"yum\"\n\t\t;;\n\t\tsles|opensuse*)\n\t\t\tinstaller=\"zypper\"\n\t\t;;\n\t\t*)\n\t\t\techo \"Unsupported distribution: $lsb_dist\"\n\t\t\texit 1\n\t\t;;\n\tesac\n\n\tlocal iter=0\n\twhile ! $sh_c \"$installer update\" && [ \"$iter\" -lt 6 ]; do\n\t\tsleep 5\n\t\titer=$((iter+1))\n\tdone\n}\n\n. /etc/iofog/agent/init.sh\ninit\ndo_check_install\ndo_install_deps\ndo_install_java"), - } - file9 := &embedded.EmbeddedFile{ - Filename: "agent/uninstall_iofog.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nAGENT_CONFIG_FOLDER=/etc/iofog-agent/\nAGENT_LOG_FOLDER=/var/log/iofog-agent/\n\ndo_uninstall_iofog() {\n\techo \"# Removing ioFog agent...\"\n\n\tcase \"$lsb_dist\" in\n\t\tubuntu|debian|raspbian)\n\t\t\t$sh_c \"apt-get -y --purge autoremove iofog-agent\"\n\t\t\t;;\n\t\tfedora|centos|rhel|ol)\n\t\t\t$sh_c \"yum remove -y iofog-agent\"\n\t\t\t;;\n\t\tsles|opensuse)\n\t\t\t$sh_c \"zypper remove -y iofog-agent\"\n\t\t\t;;\n\t\t*)\n\t\t\techo \"Error: Unsupported Linux distribution: $lsb_dist\"\n\t\t\texit 1\n\t\t\t;;\n\tesac\n\n\t# Remove config files\n\t$sh_c \"rm -rf ${AGENT_CONFIG_FOLDER}\"\n\n\t# Remove log files\n\t$sh_c \"rm -rf ${AGENT_LOG_FOLDER}\"\n}\n\n. /etc/iofog/agent/init.sh\ninit\n\ndo_uninstall_iofog"), - } - fileb := &embedded.EmbeddedFile{ - Filename: "airgap-agent/check_prereqs.sh", - FileModTime: time.Unix(1775031340, 0), - - Content: string("#!/bin/sh\nset -x\n\n# Check can sudo without password\nif ! $(sudo ls /tmp/ > /dev/null); then\n\tMSG=\"Unable to successfully use sudo with user $USER on this host.\\nUser $USER must be in sudoers group and using sudo without password must be enabled.\\nPlease see iofog.org documentation for more details.\"\n\techo $MSG\n\texit 1\nfi"), - } - filec := &embedded.EmbeddedFile{ - Filename: "airgap-agent/init.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\n# Script to detect Linux distribution and version\n# Used as a precursor for system-specific installations\n\n# Exit on error and print commands for debugging\nset -e\nset -x\n\n# Define user variable\nuser=\"$(id -un 2>/dev/null || true)\"\n\n# Check if a command exists\ncommand_exists() {\n command -v \"$@\" > /dev/null 2>&1\n}\n\n# Detect the Linux distribution\nget_distribution() {\n lsb_dist=\"\"\n dist_version=\"\"\n \n # Every system that we officially support has /etc/os-release\n if [ -r /etc/os-release ]; then\n \n lsb_dist=\"$(. /etc/os-release && echo \"$ID\")\"\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n lsb_dist=\"$(echo \"$lsb_dist\" | tr '[:upper:]' '[:lower:]')\"\n else\n echo \"Error: Unsupported Linux distribution! /etc/os-release not found.\"\n exit 1\n fi\n \n echo \"# Detected distribution: $lsb_dist (version: $dist_version)\"\n}\n\n# Check if this is a forked Linux distro\ncheck_forked() {\n # Skip if lsb_release doesn't exist\n if ! command_exists lsb_release; then\n return\n fi\n \n # Check if the `-u` option is supported\n set +e\n lsb_release -a > /dev/null 2>&1\n lsb_release_exit_code=$?\n set -e\n\n # Check if the command has exited successfully, it means we're in a forked distro\n if [ \"$lsb_release_exit_code\" = \"0\" ]; then\n # Get the upstream release info\n current_lsb_dist=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]')\n current_dist_version=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]')\n\n # Print info about current distro\n echo \"You're using '$current_lsb_dist' version '$current_dist_version'.\"\n \n # Check if current is different from detected (indicating a fork)\n if [ \"$current_lsb_dist\" != \"$lsb_dist\" ] || [ \"$current_dist_version\" != \"$dist_version\" ]; then\n echo \"Upstream release is '$lsb_dist' version '$dist_version'.\"\n fi\n else\n # Additional checks for specific distros that might not be properly detected\n if [ -r /etc/debian_version ] && [ \"$lsb_dist\" != \"ubuntu\" ] && [ \"$lsb_dist\" != \"raspbian\" ]; then\n if [ \"$lsb_dist\" = \"osmc\" ]; then\n # OSMC runs Raspbian\n lsb_dist=raspbian\n else\n # We're Debian and don't even know it!\n lsb_dist=debian\n fi\n # Get Debian version and map it to codename\n dist_version=\"$(sed 's/\\/.*//' /etc/debian_version | sed 's/\\..*//')\"\n case \"$dist_version\" in\n 14)\n dist_version=\"forky\"\n ;;\n 13)\n dist_version=\"trixie\"\n ;;\n 12)\n dist_version=\"bookworm\"\n ;;\n 11)\n dist_version=\"bullseye\"\n ;;\n 10)\n dist_version=\"buster\"\n ;;\n 9)\n dist_version=\"stretch\"\n ;;\n 8|'Kali Linux 2')\n dist_version=\"jessie\"\n ;;\n 7)\n dist_version=\"wheezy\"\n ;;\n esac\n elif [ -r /etc/redhat-release ] && [ -z \"$lsb_dist\" ]; then\n lsb_dist=redhat\n # Extract version from redhat-release file\n dist_version=\"$(sed 's/.*release \\([0-9.]*\\).*/\\1/' /etc/redhat-release)\"\n fi\n fi\n}\n\n# Set up sudo command if necessary\nsetup_sudo() {\n sh_c='sh -c'\n if [ \"$user\" != 'root' ]; then\n if command_exists sudo; then\n sh_c='sudo -E sh -c'\n elif command_exists su; then\n sh_c='su -c'\n else\n echo \"Error: this installer needs the ability to run commands as root.\"\n echo \"We are unable to find either 'sudo' or 'su' available to make this happen.\"\n exit 1\n fi\n fi\n echo \"# Using command executor: $sh_c\"\n}\n\n# Refine distribution version detection based on the distro\nrefine_distribution_version() {\n case \"$lsb_dist\" in\n ubuntu)\n if command_exists lsb_release; then\n dist_version=\"$(lsb_release --codename | cut -f2)\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/lsb-release ]; then\n \n dist_version=\"$(. /etc/lsb-release && echo \"$DISTRIB_CODENAME\")\"\n fi\n ;;\n\n debian|raspbian)\n # If we only have a number, map it to a codename for better recognition\n if echo \"$dist_version\" | grep -qE '^[0-9]+$'; then\n case \"$dist_version\" in\n 14)\n dist_version=\"forky\"\n ;;\n 13)\n dist_version=\"trixie\"\n ;;\n 12)\n dist_version=\"bookworm\"\n ;;\n 11)\n dist_version=\"bullseye\"\n ;;\n 10)\n # Handle special case for Buster\n dist_version=\"buster\"\n if [ \"$user\" = 'root' ]; then\n apt-get update --allow-releaseinfo-change || true\n elif command_exists sudo; then\n sudo apt-get update --allow-releaseinfo-change || true\n fi\n ;;\n 9)\n dist_version=\"stretch\"\n ;;\n 8)\n dist_version=\"jessie\"\n ;;\n 7)\n dist_version=\"wheezy\"\n ;;\n esac\n fi\n ;;\n\n centos|rhel|fedora|ol)\n # Make sure we have a version number\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/redhat-release ]; then\n dist_version=\"$(sed 's/.*release \\([0-9.]*\\).*/\\1/' /etc/redhat-release)\"\n fi\n ;;\n\n sles|opensuse)\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n # Fallback for older versions\n if [ -z \"$dist_version\" ] && [ -r /etc/SuSE-release ]; then\n dist_version=\"$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = //')\"\n fi\n # Ensure version is in the correct format (e.g., 15.4 for SLES 15 SP4)\n if [ -n \"$dist_version\" ]; then\n # Remove any non-numeric characters except dots\n dist_version=\"$(echo \"$dist_version\" | sed 's/[^0-9.]//g')\"\n fi\n # Normalize distribution name\n if [ \"$lsb_dist\" = \"sles\" ]; then\n lsb_dist=\"sles\"\n elif [ \"$lsb_dist\" = \"opensuse\" ]; then\n lsb_dist=\"opensuse\"\n fi\n ;;\n\n *)\n if command_exists lsb_release; then\n dist_version=\"$(lsb_release --release | cut -f2)\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n ;;\n esac\n}\n\n# Detect init system \ndetect_init_system() {\n if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then\n INIT_SYSTEM=\"systemd\"\n elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then\n INIT_SYSTEM=\"upstart\"\n elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then\n INIT_SYSTEM=\"openrc\"\n elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then\n INIT_SYSTEM=\"s6\"\n elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then\n INIT_SYSTEM=\"runit\"\n elif [ -d /etc/init.d ]; then\n INIT_SYSTEM=\"sysvinit\"\n else\n INIT_SYSTEM=\"unknown\"\n fi\n export INIT_SYSTEM\n echo \"# Detected init system: $INIT_SYSTEM\"\n}\n\n# Detect package type (deb, rpm, or other)\ndetect_package_type() {\n case \"$lsb_dist\" in\n debian|ubuntu|raspbian|mendel)\n PACKAGE_TYPE=\"deb\"\n ;;\n fedora|centos|rhel|ol|sles|opensuse*)\n PACKAGE_TYPE=\"rpm\"\n ;;\n alpine)\n PACKAGE_TYPE=\"apk\"\n ;;\n *)\n if command_exists apt-get || command_exists dpkg; then\n PACKAGE_TYPE=\"deb\"\n elif command_exists yum || command_exists dnf || command_exists zypper; then\n PACKAGE_TYPE=\"rpm\"\n elif command_exists apk; then\n PACKAGE_TYPE=\"apk\"\n else\n PACKAGE_TYPE=\"other\"\n fi\n ;;\n esac\n export PACKAGE_TYPE\n echo \"# Detected package type: $PACKAGE_TYPE\"\n}\n\n# Init function\ninit() {\n # Detect basic distribution info\n get_distribution\n \n # Set up sudo for privileged commands\n setup_sudo\n \n # Refine version information\n refine_distribution_version\n \n # Check if this is a forked distro\n check_forked\n \n # Detect init system and package type (for universal OS/init support)\n detect_init_system\n detect_package_type\n \n # Print final distribution information\n echo \"----------------------------------------\"\n echo \"Linux Distribution: $lsb_dist\"\n echo \"Version: $dist_version\"\n echo \"Init system: $INIT_SYSTEM\"\n echo \"Package type: $PACKAGE_TYPE\"\n echo \"----------------------------------------\"\n \n}\n"), - } - filed := &embedded.EmbeddedFile{ - Filename: "airgap-agent/install_container_engine.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\n# Script to configure Docker/Podman for airgap deployment\n# Check-only: verifies container engine is installed (Docker 25+ or Podman 4+), then configures/starts\n# Sources init.sh for distribution detection\n\nset -x\nset -e\n\nCONTAINER_ENGINE_MSG=\"This operating system does not support automatic container engine installation. Please install Docker 25+ or Podman 4+ on the target host and re-run, or use an airgap deployment with a pre-installed engine.\"\n\ncheck_docker_version() {\n docker_version_num=0\n if command -v docker >/dev/null 2>&1; then\n raw=$(docker -v 2>/dev/null | sed 's/.*version \\([^,]*\\),.*/\\1/' | tr -d '.')\n [ -n \"$raw\" ] && docker_version_num=\"$raw\"\n fi\n [ \"$docker_version_num\" -ge 2500 ] 2>/dev/null || return 1\n}\n\ncheck_podman_version() {\n podman_version_num=0\n if command -v podman >/dev/null 2>&1; then\n raw=$(podman --version 2>/dev/null | sed -n 's/.*version \\([0-9][0-9]*\\).*/\\1/p')\n [ -n \"$raw\" ] && podman_version_num=\"$raw\"\n fi\n [ \"$podman_version_num\" -ge 4 ] 2>/dev/null || return 1\n}\n\nstart_docker() {\n set +e\n if $sh_c \"docker ps\" >/dev/null 2>&1; then\n set -e\n return 0\n fi\n err_code=1\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd) $sh_c \"systemctl start docker\" >/dev/null 2>&1; err_code=$? ;;\n sysvinit) $sh_c \"service docker start\" >/dev/null 2>&1 || $sh_c \"/etc/init.d/docker start\" >/dev/null 2>&1; err_code=$? ;;\n openrc) $sh_c \"rc-service docker start\" >/dev/null 2>&1; err_code=$? ;;\n *)\n $sh_c \"/etc/init.d/docker start\" >/dev/null 2>&1\n err_code=$?\n [ $err_code -ne 0 ] && $sh_c \"systemctl start docker\" >/dev/null 2>&1 && err_code=0\n [ $err_code -ne 0 ] && $sh_c \"service docker start\" >/dev/null 2>&1 && err_code=0\n [ $err_code -ne 0 ] && $sh_c \"snap start docker\" >/dev/null 2>&1 && err_code=0\n ;;\n esac\n set -e\n if [ $err_code -ne 0 ]; then\n echo \"Could not start Docker daemon\"\n exit 1\n fi\n}\n\nstart_podman() {\n set +e\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl start podman\" >/dev/null 2>&1\n $sh_c \"systemctl start podman.socket\" >/dev/null 2>&1\n ;;\n sysvinit) $sh_c \"service podman start\" >/dev/null 2>&1 || $sh_c \"/etc/init.d/podman start\" >/dev/null 2>&1 ;;\n openrc) $sh_c \"rc-service podman start\" >/dev/null 2>&1 ;;\n *)\n $sh_c \"systemctl start podman\" >/dev/null 2>&1 || true\n $sh_c \"systemctl start podman.socket\" >/dev/null 2>&1 || true\n $sh_c \"service podman start\" >/dev/null 2>&1 || true\n ;;\n esac\n set -e\n}\n\ndo_modify_daemon() {\n # Skip for Podman installations\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"# Configuring Podman for CDI directory support...\"\n\n # Create CDI directories\n $sh_c \"mkdir -p /etc/cdi /var/run/cdi\"\n\n # Ensure /etc/containers exists\n $sh_c \"mkdir -p /etc/containers\"\n\n # Create containers.conf if it doesn't exist\n if [ ! -f \"/etc/containers/containers.conf\" ]; then\n $sh_c 'cat > /etc/containers/containers.conf <> /etc/containers/containers.conf'\n fi\n fi\n\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl enable podman\" 2>/dev/null || true\n $sh_c \"systemctl enable podman.socket\" 2>/dev/null || true\n ;;\n openrc) $sh_c \"rc-update add podman default\" 2>/dev/null || true ;;\n sysvinit) $sh_c \"update-rc.d podman defaults\" 2>/dev/null || $sh_c \"chkconfig podman on\" 2>/dev/null || true ;;\n *) ;;\n esac\n start_podman\n return\n fi\n \n # Original Docker daemon configuration\n if [ ! -f /etc/docker/daemon.json ]; then\n echo \"Creating /etc/docker/daemon.json...\"\n $sh_c \"mkdir -p /etc/docker\"\n $sh_c 'cat > /etc/docker/daemon.json << EOF\n{\n\t\"storage-driver\": \"overlayfs\",\n \"features\": {\n \"containerd-snapshotter\": true,\n \"cdi\": true\n },\n \"cdi-spec-dirs\": [\"/etc/cdi/\", \"/var/run/cdi\"]\n}\nEOF'\n else\n echo \"/etc/docker/daemon.json already exists\"\n fi\n echo \"Restarting Docker daemon...\"\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl daemon-reload\"\n $sh_c \"systemctl restart docker\"\n ;;\n *)\n $sh_c \"systemctl daemon-reload\" 2>/dev/null || true\n $sh_c \"systemctl restart docker\" 2>/dev/null || start_docker\n ;;\n esac\n}\n\n# Airgap: determine engine by availability (Docker 25+ or Podman 4+)\ndetermine_container_engine() {\n if check_docker_version; then\n USE_PODMAN=\"false\"\n echo \"# Using Docker (25+)\"\n elif check_podman_version; then\n USE_PODMAN=\"true\"\n echo \"# Using Podman (4+)\"\n else\n echo \"Error: Docker 25+ or Podman 4+ is required. $CONTAINER_ENGINE_MSG\"\n exit 1\n fi\n}\n\n. /etc/iofog/agent/init.sh\ninit\n\ndetermine_container_engine\n\n# Start engine and configure\nif [ \"$USE_PODMAN\" = \"false\" ]; then\n start_docker\nfi\n\ndo_modify_daemon\n\necho \"# Container engine configuration completed successfully\"\n\n"), - } - filee := &embedded.EmbeddedFile{ - Filename: "airgap-agent/install_deps.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\n\n/etc/iofog/agent/install_container_engine.sh\n"), - } - filef := &embedded.EmbeddedFile{ - Filename: "airgap-agent/install_iofog.sh", - FileModTime: time.Unix(1775031584, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nAGENT_LOG_FOLDER=/var/log/iofog-agent\nAGENT_BACKUP_FOLDER=/var/backups/iofog-agent\nAGENT_MESSAGE_FOLDER=/var/lib/iofog-agent\nAGENT_SHARE_FOLDER=/usr/share/iofog-agent\nSAVED_AGENT_CONFIG_FOLDER=/tmp/agent-config-save\nAGENT_CONTAINER_NAME=\"iofog-agent\"\nETC_DIR=/etc/iofog/agent\n\ndo_check_install() {\n\tif command_exists iofog-agent; then\n\t\tlocal VERSION=$(sudo iofog-agent version | head -n1 | sed \"s/ioFog//g\" | tr -d ' ' | tr -d \"\\n\")\n\t\tif [ \"$VERSION\" = \"$agent_version\" ]; then\n\t\t\techo \"Agent $VERSION already installed.\"\n\t\t\texit 0\n\t\tfi\n\tfi\n}\n\ndo_stop_iofog() {\n\tif ! command_exists iofog-agent; then\n\t\treturn 0\n\tfi\n\tcase \"${INIT_SYSTEM:-systemd}\" in\n\t\tsystemd)\n\t\t\tsudo systemctl stop iofog-agent 2>/dev/null || true\n\t\t\t;;\n\t\tsysvinit|openrc)\n\t\t\tsudo service iofog-agent stop 2>/dev/null || sudo /etc/init.d/iofog-agent stop 2>/dev/null || true\n\t\t\t;;\n\t\ts6)\n\t\t\tsudo s6-svc -d /etc/s6/sv/iofog-agent 2>/dev/null || true\n\t\t\t;;\n\t\trunit)\n\t\t\tsudo sv stop iofog-agent 2>/dev/null || true\n\t\t\t;;\n\t\tupstart)\n\t\t\tsudo initctl stop iofog-agent 2>/dev/null || true\n\t\t\t;;\n\t\t*)\n\t\t\tsudo systemctl stop iofog-agent 2>/dev/null || sudo service iofog-agent stop 2>/dev/null || true\n\t\t\t;;\n\tesac\n\t(docker stop ${AGENT_CONTAINER_NAME} 2>/dev/null || podman stop ${AGENT_CONTAINER_NAME} 2>/dev/null) || true\n}\n\ndo_create_env() {\nENV_FILE_NAME=iofog-agent.env # Used as an env file in systemd\n\nENV_FILE=\"$ETC_DIR/$ENV_FILE_NAME\"\n\n# Env file (for systemd)\nrm -f \"$ENV_FILE\"\ntouch \"$ENV_FILE\"\n\necho \"IOFOG_AGENT_IMAGE=${agent_image}\" >> \"$ENV_FILE\"\necho \"IOFOG_AGENT_TZ=${agent_tz}\" >> \"$ENV_FILE\"\n\n}\n\ndo_install_iofog() {\n\techo \"# Installing ioFog agent (airgap mode)...\"\n\t\n for FOLDER in ${ETC_DIR} ${AGENT_LOG_FOLDER} ${AGENT_BACKUP_FOLDER} ${AGENT_MESSAGE_FOLDER} ${AGENT_SHARE_FOLDER}; do\n if [ ! -d \"$FOLDER\" ]; then\n echo \"Creating folder: $FOLDER\"\n sudo mkdir -p \"$FOLDER\"\n sudo chmod 775 \"$FOLDER\"\n fi\n done\n\tdo_create_env\n\n USE_PODMAN=\"false\"\n case \"$lsb_dist\" in\n rhel|centos|fedora|ol|sles|opensuse*) USE_PODMAN=\"true\" ;;\n esac\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n CONTAINER_RUNTIME=\"podman\"\n SOCK_MOUNT=\"-v /run/podman/podman.sock:/run/podman/podman.sock:rw\"\n else\n CONTAINER_RUNTIME=\"docker\"\n SOCK_MOUNT=\"-v /var/run/docker.sock:/var/run/docker.sock:rw\"\n fi\n\n if [ \"${INIT_SYSTEM:-systemd}\" = \"systemd\" ]; then\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"Using Podman (Quadlet) for container management...\"\n sudo mkdir -p /etc/containers/systemd\n cat < /dev/null\n[Unit]\nDescription=ioFog Agent Service\nAfter=podman.service\nRequires=podman.service\n\n[Container]\nContainerName=${AGENT_CONTAINER_NAME}\nImage=${agent_image}\nPodmanArgs=--privileged --stop-timeout=60\nEnvironmentFile=${ETC_DIR}/iofog-agent.env\nNetwork=host\nVolume=/run/podman/podman.sock:/run/podman/podman.sock:rw\nVolume=iofog-agent-config:/etc/iofog-agent:rw\nVolume=/var/log/iofog-agent:/var/log/iofog-agent:rw\nVolume=/var/backups/iofog-agent:/var/backups/iofog-agent:rw\nVolume=/usr/share/iofog-agent:/usr/share/iofog-agent:rw\nVolume=/var/lib/iofog-agent:/var/lib/iofog-agent:rw\nLogDriver=journald\n\n[Service]\nRestart=always\n\n[Install]\nWantedBy=default.target\nEOF\n sudo systemctl daemon-reload\n sudo systemctl restart podman 2>/dev/null || true\n sudo systemctl enable iofog-agent.service\n sudo systemctl start iofog-agent.service\n else\n echo \"Using Docker (systemd) for container management...\"\n cat < /dev/null\n[Unit]\nDescription=ioFog Agent Service\nAfter=docker.service\nRequires=docker.service\n\n[Service]\nRestart=always\nExecStartPre=-/usr/bin/docker rm -f ${AGENT_CONTAINER_NAME}\nExecStart=/usr/bin/docker run --rm --name ${AGENT_CONTAINER_NAME} \\\\\n--env-file ${ETC_DIR}/iofog-agent.env \\\\\n-v /var/run/docker.sock:/var/run/docker.sock:rw \\\\\n-v iofog-agent-config:/etc/iofog-agent:rw \\\\\n-v /var/log/iofog-agent:/var/log/iofog-agent:rw \\\\\n-v /var/backups/iofog-agent:/var/backups/iofog-agent:rw \\\\\n-v /usr/share/iofog-agent:/usr/share/iofog-agent:rw \\\\\n-v /var/lib/iofog-agent:/var/lib/iofog-agent:rw \\\\\n--net=host \\\\\n--privileged \\\\\n--stop-timeout 60 \\\\\n--attach stdout \\\\\n--attach stderr \\\\\n${agent_image}\nExecStop=/usr/bin/docker stop ${AGENT_CONTAINER_NAME}\n\n[Install]\nWantedBy=default.target\nEOF\n sudo systemctl daemon-reload\n sudo systemctl enable iofog-agent.service\n sudo systemctl start iofog-agent.service\n fi\n else\n echo \"Using $CONTAINER_RUNTIME with $INIT_SYSTEM for container management...\"\n RUN_CMD=\"${CONTAINER_RUNTIME} run --rm -d --name ${AGENT_CONTAINER_NAME} --env-file ${ETC_DIR}/iofog-agent.env ${SOCK_MOUNT} -v iofog-agent-config:/etc/iofog-agent:rw -v /var/log/iofog-agent:/var/log/iofog-agent:rw -v /var/backups/iofog-agent:/var/backups/iofog-agent:rw -v /usr/share/iofog-agent:/usr/share/iofog-agent:rw -v /var/lib/iofog-agent:/var/lib/iofog-agent:rw --net=host --privileged --stop-timeout 60 ${agent_image}\"\n RUN_CMD_FG=\"${CONTAINER_RUNTIME} run --rm --name ${AGENT_CONTAINER_NAME} --env-file ${ETC_DIR}/iofog-agent.env ${SOCK_MOUNT} -v iofog-agent-config:/etc/iofog-agent:rw -v /var/log/iofog-agent:/var/log/iofog-agent:rw -v /var/backups/iofog-agent:/var/backups/iofog-agent:rw -v /usr/share/iofog-agent:/usr/share/iofog-agent:rw -v /var/lib/iofog-agent:/var/lib/iofog-agent:rw --net=host --privileged --stop-timeout 60 ${agent_image}\"\n STOP_CMD=\"${CONTAINER_RUNTIME} stop ${AGENT_CONTAINER_NAME}\"\n case \"$INIT_SYSTEM\" in\n sysvinit|openrc)\n sudo tee /etc/init.d/iofog-agent > /dev/null </dev/null | grep -q \"^${AGENT_CONTAINER_NAME}\\$\"; then exit 0; fi\n $RUN_CMD\n ;;\n stop) $STOP_CMD 2>/dev/null || true ;;\n restart) \\$0 stop; \\$0 start ;;\n status)\n if ${CONTAINER_RUNTIME} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^${AGENT_CONTAINER_NAME}\\$\"; then echo \"running\"; exit 0; else echo \"stopped\"; exit 1; fi\n ;;\n *) echo \"Usage: \\$0 {start|stop|restart|status}\"; exit 1 ;;\nesac\nexit 0\nINITSCRIPT\n sudo chmod +x /etc/init.d/iofog-agent\n if [ \"$INIT_SYSTEM\" = \"openrc\" ]; then\n sudo rc-update add iofog-agent default 2>/dev/null || true\n sudo rc-service iofog-agent start\n else\n sudo update-rc.d iofog-agent defaults 2>/dev/null || sudo chkconfig iofog-agent on 2>/dev/null || true\n sudo service iofog-agent start 2>/dev/null || sudo /etc/init.d/iofog-agent start\n fi\n ;;\n s6)\n sudo mkdir -p /etc/s6/sv/iofog-agent\n printf '#!/bin/sh\\nexec %s\\n' \"$RUN_CMD_FG\" | sudo tee /etc/s6/sv/iofog-agent/run > /dev/null\n sudo chmod +x /etc/s6/sv/iofog-agent/run\n [ -d /etc/s6/adminsv/default ] && sudo ln -sf /etc/s6/sv/iofog-agent /etc/s6/adminsv/default/iofog-agent 2>/dev/null || true\n sudo s6-svc -u /etc/s6/sv/iofog-agent 2>/dev/null || true\n ;;\n runit)\n sudo mkdir -p /etc/runit/sv/iofog-agent\n printf '#!/bin/sh\\nexec %s\\n' \"$RUN_CMD_FG\" | sudo tee /etc/runit/sv/iofog-agent/run > /dev/null\n sudo chmod +x /etc/runit/sv/iofog-agent/run\n [ -d /var/service ] && sudo ln -sf /etc/runit/sv/iofog-agent /var/service/iofog-agent 2>/dev/null || true\n [ -d /etc/runit/runsvdir/default ] && sudo ln -sf /etc/runit/sv/iofog-agent /etc/runit/runsvdir/default/iofog-agent 2>/dev/null || true\n sudo sv start iofog-agent 2>/dev/null || true\n ;;\n upstart)\n printf 'description \"IoFog Agent container\"\\nstart on runlevel [2345]\\nstop on runlevel [!2345]\\nrespawn\\nrespawn limit 10 5\\nexec %s\\n' \"$RUN_CMD_FG\" | sudo tee /etc/init/iofog-agent.conf > /dev/null\n sudo initctl reload-configuration 2>/dev/null || true\n sudo initctl start iofog-agent 2>/dev/null || true\n ;;\n *)\n sudo tee /etc/init.d/iofog-agent > /dev/null < /dev/null\n#!/bin/sh\nCONTAINER_NAME=\"iofog-agent\"\nif ! podman ps --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Error: The iofog-agent container is not running.\"\n exit 1\nfi\nexec podman exec ${CONTAINER_NAME} iofog-agent \"$@\"\nEOF\n else\n cat <<'EOF' | sudo tee ${EXECUTABLE_FILE} > /dev/null\n#!/bin/sh\nCONTAINER_NAME=\"iofog-agent\"\nif ! docker ps --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Error: The iofog-agent container is not running.\"\n exit 1\nfi\nexec docker exec ${CONTAINER_NAME} iofog-agent \"$@\"\nEOF\n fi\n sudo chmod +x ${EXECUTABLE_FILE}\n\n echo \"ioFog agent installation completed!\"\n}\n\ndo_start_iofog(){\n\tcase \"${INIT_SYSTEM:-systemd}\" in\n\t\tsystemd) sudo systemctl start iofog-agent >/dev/null 2>&1 & ;;\n\t\tsysvinit|openrc) sudo service iofog-agent start 2>/dev/null || sudo /etc/init.d/iofog-agent start 2>/dev/null & ;;\n\t\ts6) sudo s6-svc -u /etc/s6/sv/iofog-agent 2>/dev/null & ;;\n\t\trunit) sudo sv start iofog-agent 2>/dev/null & ;;\n\t\tupstart) sudo initctl start iofog-agent 2>/dev/null & ;;\n\t\t*) sudo systemctl start iofog-agent 2>/dev/null || sudo /etc/init.d/iofog-agent start 2>/dev/null & ;;\n\tesac\n\tlocal STATUS=\"\"\n\tlocal ITER=0\n\twhile [ \"$STATUS\" != \"RUNNING\" ]; do\n\t\tITER=$((ITER+1))\n\t\tif [ \"$ITER\" -gt 600 ]; then\n\t\t\techo \"Timed out waiting for Agent to be RUNNING\"\n\t\t\texit 1\n\t\tfi\n\t\tsleep 1\n\t\tSTATUS=$(sudo iofog-agent status 2>/dev/null | cut -f2 -d: | head -n 1 | tr -d '[:space:]')\n\t\techo \"${STATUS}\"\n\tdone\n\tsudo iofog-agent \"config -cf 10 -sf 10\"\n\tif [ \"$lsb_dist\" = \"rhel\" ] || [ \"$lsb_dist\" = \"centos\" ] || [ \"$lsb_dist\" = \"fedora\" ] || [ \"$lsb_dist\" = \"ol\" ] || [ \"$lsb_dist\" = \"sles\" ] || [ \"$lsb_dist\" = \"opensuse\" ]; then\n\t\tsudo iofog-agent \"config -c unix:///var/run/podman/podman.sock\"\n\tfi\n}\n\nagent_image=\"$1\"\nagent_tz=\"$2\"\necho \"Using variables\"\necho \"version: $agent_image\"\necho \"timezone: $agent_tz\"\n. /etc/iofog/agent/init.sh\ninit\ndo_check_install\ndo_stop_iofog\ndo_install_iofog\ndo_start_iofog\n\n\n\n"), - } - fileg := &embedded.EmbeddedFile{ - Filename: "airgap-agent/uninstall_iofog.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nAGENT_CONFIG_FOLDER=iofog-agent-config\nAGENT_LOG_FOLDER=/var/log/iofog-agent\nAGENT_BACKUP_FOLDER=/var/backups/iofog-agent\nAGENT_MESSAGE_FOLDER=/var/lib/iofog-agent\nEXECUTABLE_FILE=/usr/local/bin/iofog-agent\nCONTAINER_NAME=\"iofog-agent\"\n\ndo_uninstall_iofog() {\n echo \"# Removing ioFog agent...\"\n\n case \"$lsb_dist\" in\n rhel|fedora|centos|ol|sles|opensuse*) CONTAINER_RUNTIME=\"podman\" ;;\n *) CONTAINER_RUNTIME=\"docker\" ;;\n esac\n\n case \"${INIT_SYSTEM:-systemd}\" in\n systemd)\n for f in /etc/systemd/system/iofog-agent.service /etc/containers/systemd/iofog-agent.container; do\n if [ -f \"$f\" ]; then\n echo \"Disabling and stopping systemd service...\"\n sudo systemctl stop iofog-agent.service 2>/dev/null || true\n sudo systemctl disable iofog-agent.service 2>/dev/null || true\n sudo rm -f \"$f\"\n sudo systemctl daemon-reload\n break\n fi\n done\n ;;\n sysvinit|openrc)\n if [ -f /etc/init.d/iofog-agent ]; then\n sudo service iofog-agent stop 2>/dev/null || sudo /etc/init.d/iofog-agent stop 2>/dev/null || true\n [ \"$INIT_SYSTEM\" = \"openrc\" ] && sudo rc-update del iofog-agent default 2>/dev/null || true\n sudo update-rc.d -f iofog-agent remove 2>/dev/null || sudo chkconfig --del iofog-agent 2>/dev/null || true\n sudo rm -f /etc/init.d/iofog-agent\n fi\n ;;\n s6)\n sudo s6-svc -d /etc/s6/sv/iofog-agent 2>/dev/null || true\n sudo rm -rf /etc/s6/sv/iofog-agent\n [ -L /etc/s6/adminsv/default/iofog-agent ] && sudo rm -f /etc/s6/adminsv/default/iofog-agent\n ;;\n runit)\n sudo sv stop iofog-agent 2>/dev/null || true\n [ -L /var/service/iofog-agent ] && sudo rm -f /var/service/iofog-agent\n [ -L /etc/runit/runsvdir/default/iofog-agent ] && sudo rm -f /etc/runit/runsvdir/default/iofog-agent\n sudo rm -rf /etc/runit/sv/iofog-agent\n ;;\n upstart)\n sudo initctl stop iofog-agent 2>/dev/null || true\n sudo rm -f /etc/init/iofog-agent.conf\n ;;\n *)\n sudo systemctl stop iofog-agent 2>/dev/null || true\n sudo systemctl disable iofog-agent 2>/dev/null || true\n sudo rm -f /etc/systemd/system/iofog-agent.service /etc/containers/systemd/iofog-agent.container\n sudo systemctl daemon-reload 2>/dev/null || true\n [ -f /etc/init.d/iofog-agent ] && sudo /etc/init.d/iofog-agent stop 2>/dev/null || true\n sudo rm -f /etc/init.d/iofog-agent\n ;;\n esac\n\n if sudo ${CONTAINER_RUNTIME} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Stopping and removing the ioFog agent container...\"\n sudo ${CONTAINER_RUNTIME} stop ${CONTAINER_NAME} 2>/dev/null || true\n sudo ${CONTAINER_RUNTIME} rm ${CONTAINER_NAME} 2>/dev/null || true\n fi\n\n # Remove config files\n echo \"Checking if the ${CONTAINER_RUNTIME} volume exists...\"\n\n if sudo ${CONTAINER_RUNTIME} volume inspect \"${AGENT_CONFIG_FOLDER}\" >/dev/null 2>&1; then\n echo \"${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' found. Removing...\"\n sudo ${CONTAINER_RUNTIME} volume rm \"${AGENT_CONFIG_FOLDER}\"\n echo \"${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' has been removed.\"\n else\n echo \"${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' does not exist. Skipping removal.\"\n fi\n\n # Remove log files\n echo \"Removing log files...\"\n sudo rm -rf ${AGENT_LOG_FOLDER}\n\n # Remove backup files\n echo \"Removing backup files...\"\n sudo rm -rf ${AGENT_BACKUP_FOLDER}\n\n # Remove message files\n echo \"Removing message files...\"\n sudo rm -rf ${AGENT_MESSAGE_FOLDER}\n\n # Remove the executable script\n if [ -f ${EXECUTABLE_FILE} ]; then\n echo \"Removing the iofog-agent executable script...\"\n sudo rm -f ${EXECUTABLE_FILE}\n fi\n\n echo \"ioFog agent uninstalled successfully!\"\n}\n\n. /etc/iofog/agent/init.sh\ninit\n\ndo_uninstall_iofog\n"), - } - filei := &embedded.EmbeddedFile{ - Filename: "airgap-controller/check_prereqs.sh", - FileModTime: time.Unix(1775031345, 0), - - Content: string("#!/bin/sh\nset -x\n\n# Check can sudo without password\nif ! $(sudo ls /tmp/ > /dev/null); then\n\tMSG=\"Unable to successfully use sudo with user $USER on this host.\\nUser $USER must be in sudoers group and using sudo without password must be enabled.\\nPlease see iofog.org documentation for more details.\"\n\techo $MSG\n\texit 1\nfi"), - } - filej := &embedded.EmbeddedFile{ - Filename: "airgap-controller/init.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\n# Script to detect Linux distribution and version\n# Used as a precursor for system-specific installations\n\n# Exit on error and print commands for debugging\nset -e\nset -x\n\n# Define user variable\nuser=\"$(id -un 2>/dev/null || true)\"\n\n# Check if a command exists\ncommand_exists() {\n command -v \"$@\" > /dev/null 2>&1\n}\n\n# Detect the Linux distribution\nget_distribution() {\n lsb_dist=\"\"\n dist_version=\"\"\n \n # Every system that we officially support has /etc/os-release\n if [ -r /etc/os-release ]; then\n \n lsb_dist=\"$(. /etc/os-release && echo \"$ID\")\"\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n lsb_dist=\"$(echo \"$lsb_dist\" | tr '[:upper:]' '[:lower:]')\"\n else\n echo \"Error: Unsupported Linux distribution! /etc/os-release not found.\"\n exit 1\n fi\n \n echo \"# Detected distribution: $lsb_dist (version: $dist_version)\"\n}\n\n# Check if this is a forked Linux distro\ncheck_forked() {\n # Skip if lsb_release doesn't exist\n if ! command_exists lsb_release; then\n return\n fi\n \n # Check if the `-u` option is supported\n set +e\n lsb_release -a > /dev/null 2>&1\n lsb_release_exit_code=$?\n set -e\n\n # Check if the command has exited successfully, it means we're in a forked distro\n if [ \"$lsb_release_exit_code\" = \"0\" ]; then\n # Get the upstream release info\n current_lsb_dist=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]')\n current_dist_version=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]')\n\n # Print info about current distro\n echo \"You're using '$current_lsb_dist' version '$current_dist_version'.\"\n \n # Check if current is different from detected (indicating a fork)\n if [ \"$current_lsb_dist\" != \"$lsb_dist\" ] || [ \"$current_dist_version\" != \"$dist_version\" ]; then\n echo \"Upstream release is '$lsb_dist' version '$dist_version'.\"\n fi\n else\n # Additional checks for specific distros that might not be properly detected\n if [ -r /etc/debian_version ] && [ \"$lsb_dist\" != \"ubuntu\" ] && [ \"$lsb_dist\" != \"raspbian\" ]; then\n if [ \"$lsb_dist\" = \"osmc\" ]; then\n # OSMC runs Raspbian\n lsb_dist=raspbian\n else\n # We're Debian and don't even know it!\n lsb_dist=debian\n fi\n # Get Debian version and map it to codename\n dist_version=\"$(sed 's/\\/.*//' /etc/debian_version | sed 's/\\..*//')\"\n case \"$dist_version\" in\n 14)\n dist_version=\"forky\"\n ;;\n 13)\n dist_version=\"trixie\"\n ;;\n 12)\n dist_version=\"bookworm\"\n ;;\n 11)\n dist_version=\"bullseye\"\n ;;\n 10)\n dist_version=\"buster\"\n ;;\n 9)\n dist_version=\"stretch\"\n ;;\n 8|'Kali Linux 2')\n dist_version=\"jessie\"\n ;;\n 7)\n dist_version=\"wheezy\"\n ;;\n esac\n elif [ -r /etc/redhat-release ] && [ -z \"$lsb_dist\" ]; then\n lsb_dist=redhat\n # Extract version from redhat-release file\n dist_version=\"$(sed 's/.*release \\([0-9.]*\\).*/\\1/' /etc/redhat-release)\"\n fi\n fi\n}\n\n# Set up sudo command if necessary\nsetup_sudo() {\n sh_c='sh -c'\n if [ \"$user\" != 'root' ]; then\n if command_exists sudo; then\n sh_c='sudo -E sh -c'\n elif command_exists su; then\n sh_c='su -c'\n else\n echo \"Error: this installer needs the ability to run commands as root.\"\n echo \"We are unable to find either 'sudo' or 'su' available to make this happen.\"\n exit 1\n fi\n fi\n echo \"# Using command executor: $sh_c\"\n}\n\n# Refine distribution version detection based on the distro\nrefine_distribution_version() {\n case \"$lsb_dist\" in\n ubuntu)\n if command_exists lsb_release; then\n dist_version=\"$(lsb_release --codename | cut -f2)\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/lsb-release ]; then\n \n dist_version=\"$(. /etc/lsb-release && echo \"$DISTRIB_CODENAME\")\"\n fi\n ;;\n\n debian|raspbian)\n # If we only have a number, map it to a codename for better recognition\n if echo \"$dist_version\" | grep -qE '^[0-9]+$'; then\n case \"$dist_version\" in\n 14)\n dist_version=\"forky\"\n ;;\n 13)\n dist_version=\"trixie\"\n ;;\n 12)\n dist_version=\"bookworm\"\n ;;\n 11)\n dist_version=\"bullseye\"\n ;;\n 10)\n # Handle special case for Buster\n dist_version=\"buster\"\n if [ \"$user\" = 'root' ]; then\n apt-get update --allow-releaseinfo-change || true\n elif command_exists sudo; then\n sudo apt-get update --allow-releaseinfo-change || true\n fi\n ;;\n 9)\n dist_version=\"stretch\"\n ;;\n 8)\n dist_version=\"jessie\"\n ;;\n 7)\n dist_version=\"wheezy\"\n ;;\n esac\n fi\n ;;\n\n centos|rhel|fedora|ol)\n # Make sure we have a version number\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/redhat-release ]; then\n dist_version=\"$(sed 's/.*release \\([0-9.]*\\).*/\\1/' /etc/redhat-release)\"\n fi\n ;;\n\n sles|opensuse)\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n # Fallback for older versions\n if [ -z \"$dist_version\" ] && [ -r /etc/SuSE-release ]; then\n dist_version=\"$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = //')\"\n fi\n # Ensure version is in the correct format (e.g., 15.4 for SLES 15 SP4)\n if [ -n \"$dist_version\" ]; then\n # Remove any non-numeric characters except dots\n dist_version=\"$(echo \"$dist_version\" | sed 's/[^0-9.]//g')\"\n fi\n # Normalize distribution name\n if [ \"$lsb_dist\" = \"sles\" ]; then\n lsb_dist=\"sles\"\n elif [ \"$lsb_dist\" = \"opensuse\" ]; then\n lsb_dist=\"opensuse\"\n fi\n ;;\n\n *)\n if command_exists lsb_release; then\n dist_version=\"$(lsb_release --release | cut -f2)\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n ;;\n esac\n}\n\n# Detect init system \ndetect_init_system() {\n if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then\n INIT_SYSTEM=\"systemd\"\n elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then\n INIT_SYSTEM=\"upstart\"\n elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then\n INIT_SYSTEM=\"openrc\"\n elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then\n INIT_SYSTEM=\"s6\"\n elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then\n INIT_SYSTEM=\"runit\"\n elif [ -d /etc/init.d ]; then\n INIT_SYSTEM=\"sysvinit\"\n else\n INIT_SYSTEM=\"unknown\"\n fi\n export INIT_SYSTEM\n echo \"# Detected init system: $INIT_SYSTEM\"\n}\n\n# Detect package type (deb, rpm, or other)\ndetect_package_type() {\n case \"$lsb_dist\" in\n debian|ubuntu|raspbian|mendel)\n PACKAGE_TYPE=\"deb\"\n ;;\n fedora|centos|rhel|ol|sles|opensuse*)\n PACKAGE_TYPE=\"rpm\"\n ;;\n alpine)\n PACKAGE_TYPE=\"apk\"\n ;;\n *)\n if command_exists apt-get || command_exists dpkg; then\n PACKAGE_TYPE=\"deb\"\n elif command_exists yum || command_exists dnf || command_exists zypper; then\n PACKAGE_TYPE=\"rpm\"\n elif command_exists apk; then\n PACKAGE_TYPE=\"apk\"\n else\n PACKAGE_TYPE=\"other\"\n fi\n ;;\n esac\n export PACKAGE_TYPE\n echo \"# Detected package type: $PACKAGE_TYPE\"\n}\n\n# Init function\ninit() {\n # Detect basic distribution info\n get_distribution\n \n # Set up sudo for privileged commands\n setup_sudo\n \n # Refine version information\n refine_distribution_version\n \n # Check if this is a forked distro\n check_forked\n \n # Detect init system and package type (for universal OS/init support)\n detect_init_system\n detect_package_type\n \n # Print final distribution information\n echo \"----------------------------------------\"\n echo \"Linux Distribution: $lsb_dist\"\n echo \"Version: $dist_version\"\n echo \"Init system: $INIT_SYSTEM\"\n echo \"Package type: $PACKAGE_TYPE\"\n echo \"----------------------------------------\"\n \n}\n"), - } - filek := &embedded.EmbeddedFile{ - Filename: "airgap-controller/install_container_engine.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\n# Script to configure Docker/Podman for airgap deployment\n# Check-only: verifies container engine is installed (Docker 25+ or Podman 4+), then configures/starts\n# Sources init.sh for distribution detection\n\nset -x\nset -e\n\nCONTAINER_ENGINE_MSG=\"This operating system does not support automatic container engine installation. Please install Docker 25+ or Podman 4+ on the target host and re-run, or use an airgap deployment with a pre-installed engine.\"\n\ncheck_docker_version() {\n docker_version_num=0\n if command -v docker >/dev/null 2>&1; then\n raw=$(docker -v 2>/dev/null | sed 's/.*version \\([^,]*\\),.*/\\1/' | tr -d '.')\n [ -n \"$raw\" ] && docker_version_num=\"$raw\"\n fi\n [ \"$docker_version_num\" -ge 2500 ] 2>/dev/null || return 1\n}\n\ncheck_podman_version() {\n podman_version_num=0\n if command -v podman >/dev/null 2>&1; then\n raw=$(podman --version 2>/dev/null | sed -n 's/.*version \\([0-9][0-9]*\\).*/\\1/p')\n [ -n \"$raw\" ] && podman_version_num=\"$raw\"\n fi\n [ \"$podman_version_num\" -ge 4 ] 2>/dev/null || return 1\n}\n\nstart_docker() {\n set +e\n if $sh_c \"docker ps\" >/dev/null 2>&1; then\n set -e\n return 0\n fi\n err_code=1\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd) $sh_c \"systemctl start docker\" >/dev/null 2>&1; err_code=$? ;;\n sysvinit) $sh_c \"service docker start\" >/dev/null 2>&1 || $sh_c \"/etc/init.d/docker start\" >/dev/null 2>&1; err_code=$? ;;\n openrc) $sh_c \"rc-service docker start\" >/dev/null 2>&1; err_code=$? ;;\n *)\n $sh_c \"/etc/init.d/docker start\" >/dev/null 2>&1\n err_code=$?\n [ $err_code -ne 0 ] && $sh_c \"systemctl start docker\" >/dev/null 2>&1 && err_code=0\n [ $err_code -ne 0 ] && $sh_c \"service docker start\" >/dev/null 2>&1 && err_code=0\n [ $err_code -ne 0 ] && $sh_c \"snap start docker\" >/dev/null 2>&1 && err_code=0\n ;;\n esac\n set -e\n if [ $err_code -ne 0 ]; then\n echo \"Could not start Docker daemon\"\n exit 1\n fi\n}\n\nstart_podman() {\n set +e\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl start podman\" >/dev/null 2>&1\n $sh_c \"systemctl start podman.socket\" >/dev/null 2>&1\n ;;\n sysvinit) $sh_c \"service podman start\" >/dev/null 2>&1 || $sh_c \"/etc/init.d/podman start\" >/dev/null 2>&1 ;;\n openrc) $sh_c \"rc-service podman start\" >/dev/null 2>&1 ;;\n *)\n $sh_c \"systemctl start podman\" >/dev/null 2>&1 || true\n $sh_c \"systemctl start podman.socket\" >/dev/null 2>&1 || true\n $sh_c \"service podman start\" >/dev/null 2>&1 || true\n ;;\n esac\n set -e\n}\n\ndo_modify_daemon() {\n # Skip for Podman installations\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"# Configuring Podman for CDI directory support...\"\n\n # Create CDI directories\n $sh_c \"mkdir -p /etc/cdi /var/run/cdi\"\n\n # Ensure /etc/containers exists\n $sh_c \"mkdir -p /etc/containers\"\n\n # Create containers.conf if it doesn't exist\n if [ ! -f \"/etc/containers/containers.conf\" ]; then\n $sh_c 'cat > /etc/containers/containers.conf <> /etc/containers/containers.conf'\n fi\n fi\n\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl enable podman\" 2>/dev/null || true\n $sh_c \"systemctl enable podman.socket\" 2>/dev/null || true\n ;;\n openrc) $sh_c \"rc-update add podman default\" 2>/dev/null || true ;;\n sysvinit) $sh_c \"update-rc.d podman defaults\" 2>/dev/null || $sh_c \"chkconfig podman on\" 2>/dev/null || true ;;\n *) ;;\n esac\n start_podman\n return\n fi\n \n # Original Docker daemon configuration\n if [ ! -f /etc/docker/daemon.json ]; then\n echo \"Creating /etc/docker/daemon.json...\"\n $sh_c \"mkdir -p /etc/docker\"\n $sh_c 'cat > /etc/docker/daemon.json << EOF\n{\n\t\"storage-driver\": \"overlayfs\",\n \"features\": {\n \"containerd-snapshotter\": true,\n \"cdi\": true\n },\n \"cdi-spec-dirs\": [\"/etc/cdi/\", \"/var/run/cdi\"]\n}\nEOF'\n else\n echo \"/etc/docker/daemon.json already exists\"\n fi\n echo \"Restarting Docker daemon...\"\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl daemon-reload\"\n $sh_c \"systemctl restart docker\"\n ;;\n *)\n $sh_c \"systemctl daemon-reload\" 2>/dev/null || true\n $sh_c \"systemctl restart docker\" 2>/dev/null || start_docker\n ;;\n esac\n}\n\n# Airgap: determine engine by availability (Docker 25+ or Podman 4+)\ndetermine_container_engine() {\n if check_docker_version; then\n USE_PODMAN=\"false\"\n echo \"# Using Docker (25+)\"\n elif check_podman_version; then\n USE_PODMAN=\"true\"\n echo \"# Using Podman (4+)\"\n else\n echo \"Error: Docker 25+ or Podman 4+ is required. $CONTAINER_ENGINE_MSG\"\n exit 1\n fi\n}\n\n. /etc/iofog/controller/init.sh\ninit\n\ndetermine_container_engine\n\nif [ \"$USE_PODMAN\" = \"false\" ]; then\n start_docker\nfi\n\ndo_modify_daemon\n\necho \"# Container engine configuration completed successfully\"\n\n\n\n"), - } - filel := &embedded.EmbeddedFile{ - Filename: "airgap-controller/install_iofog.sh", - FileModTime: time.Unix(1775031610, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\n# INSTALL_DIR=\"/opt/iofog\"\nTMP_DIR=\"/tmp/iofog\"\nETC_DIR=\"/etc/iofog/controller\"\nCONTROLLER_LOG_FOLDER=/var/log/iofog-controller\nCONTROLLER_CONTAINER_NAME=\"iofog-controller\"\n\ncommand_exists() {\n command -v \"$1\" >/dev/null 2>&1\n}\n\ndo_stop_iofog_controller() {\n if ! command_exists iofog-controller; then\n return 0\n fi\n case \"${INIT_SYSTEM:-systemd}\" in\n systemd) sudo systemctl stop iofog-controller 2>/dev/null || true ;;\n sysvinit|openrc) sudo service iofog-controller stop 2>/dev/null || sudo /etc/init.d/iofog-controller stop 2>/dev/null || true ;;\n s6) sudo s6-svc -d /etc/s6/sv/iofog-controller 2>/dev/null || true ;;\n runit) sudo sv stop iofog-controller 2>/dev/null || true ;;\n upstart) sudo initctl stop iofog-controller 2>/dev/null || true ;;\n *) sudo systemctl stop iofog-controller 2>/dev/null || sudo service iofog-controller stop 2>/dev/null || true ;;\n esac\n (docker stop ${CONTROLLER_CONTAINER_NAME} 2>/dev/null || podman stop ${CONTROLLER_CONTAINER_NAME} 2>/dev/null) || true\n}\n\ndo_install_iofog_controller() {\n echo \"# Installing ioFog controller (airgap mode)...\"\n\n for FOLDER in ${ETC_DIR} ${CONTROLLER_LOG_FOLDER}; do\n if [ ! -d \"$FOLDER\" ]; then\n echo \"Creating folder: $FOLDER\"\n sudo mkdir -p \"$FOLDER\"\n sudo chmod 775 \"$FOLDER\"\n fi\n done\n\n USE_PODMAN=\"false\"\n case \"$lsb_dist\" in\n rhel|centos|fedora|ol|sles|opensuse*) USE_PODMAN=\"true\" ;;\n esac\n\n CONTROLLER_RUN_ARGS=\"-e IOFOG_CONTROLLER_IMAGE=${controller_image} --env-file ${ETC_DIR}/iofog-controller.env -v iofog-controller-db:/home/runner/.npm-global/lib/node_modules/@eclipse-iofog/iofogcontroller/src/data/sqlite_files/:rw -v iofog-controller-log:/var/log/iofog-controller:rw -p 51121:51121 -p 80:8008 --stop-timeout 60 ${controller_image}\"\n\n if [ \"${INIT_SYSTEM:-systemd}\" = \"systemd\" ]; then\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"Creating Quadlet container file for ioFog controller...\"\n sudo mkdir -p /etc/containers/systemd\n cat < /dev/null\n[Unit]\nDescription=ioFog Controller Service\nAfter=podman.service\nRequires=podman.service\n\n[Container]\nContainerName=${CONTROLLER_CONTAINER_NAME}\nImage=${controller_image}\nPodmanArgs=--stop-timeout=60\nEnvironment=IOFOG_CONTROLLER_IMAGE=${controller_image}\nEnvironmentFile=${ETC_DIR}/iofog-controller.env\nVolume=iofog-controller-db:/home/runner/.npm-global/lib/node_modules/@eclipse-iofog/iofogcontroller/src/data/sqlite_files/:rw\nVolume=iofog-controller-log:/var/log/iofog-controller:rw\nPublishPort=51121:51121\nPublishPort=80:8008\nLogDriver=journald\n\n[Service]\nRestart=always\n\n[Install]\nWantedBy=default.target\nEOF\n sudo systemctl daemon-reload\n sudo systemctl restart podman 2>/dev/null || true\n sudo systemctl enable iofog-controller.service\n sudo systemctl start iofog-controller.service\n else\n echo \"Creating systemd service for ioFog controller...\"\n cat < /dev/null\n[Unit]\nDescription=ioFog Controller Service\nAfter=docker.service\nRequires=docker.service\n\n[Service]\nTimeoutStartSec=0\nRestart=always\nExecStartPre=-/usr/bin/docker rm -f ${CONTROLLER_CONTAINER_NAME}\nExecStart=/usr/bin/docker run --rm --name ${CONTROLLER_CONTAINER_NAME} \\\\\n${CONTROLLER_RUN_ARGS}\nExecStop=/usr/bin/docker stop ${CONTROLLER_CONTAINER_NAME}\n\n[Install]\nWantedBy=default.target\nEOF\n sudo systemctl daemon-reload\n sudo systemctl enable iofog-controller.service\n sudo systemctl start iofog-controller.service\n fi\n else\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n RUN_CMD=\"podman run --rm -d --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}\"\n RUN_CMD_FG=\"podman run --rm --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}\"\n else\n RUN_CMD=\"docker run --rm -d --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}\"\n RUN_CMD_FG=\"docker run --rm --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}\"\n fi\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n STOP_CMD=\"podman stop ${CONTROLLER_CONTAINER_NAME}\"\n else\n STOP_CMD=\"docker stop ${CONTROLLER_CONTAINER_NAME}\"\n fi\n case \"$INIT_SYSTEM\" in\n sysvinit|openrc)\n sudo tee /etc/init.d/iofog-controller > /dev/null </dev/null | grep -q \"^${CONTROLLER_CONTAINER_NAME}\\$\"; then exit 0; fi\n if podman ps --format '{{.Names}}' 2>/dev/null | grep -q \"^${CONTROLLER_CONTAINER_NAME}\\$\"; then exit 0; fi\n $RUN_CMD\n ;;\n stop) $STOP_CMD 2>/dev/null || true ;;\n restart) \\$0 stop; \\$0 start ;;\n status)\n if docker ps --format '{{.Names}}' 2>/dev/null | grep -q \"^${CONTROLLER_CONTAINER_NAME}\\$\"; then echo \"running\"; exit 0; fi\n if podman ps --format '{{.Names}}' 2>/dev/null | grep -q \"^${CONTROLLER_CONTAINER_NAME}\\$\"; then echo \"running\"; exit 0; fi\n echo \"stopped\"; exit 1\n ;;\n *) echo \"Usage: \\$0 {start|stop|restart|status}\"; exit 1 ;;\nesac\nexit 0\nINITSCRIPT\n sudo chmod +x /etc/init.d/iofog-controller\n if [ \"$INIT_SYSTEM\" = \"openrc\" ]; then\n sudo rc-update add iofog-controller default 2>/dev/null || true\n sudo rc-service iofog-controller start\n else\n sudo update-rc.d iofog-controller defaults 2>/dev/null || sudo chkconfig iofog-controller on 2>/dev/null || true\n sudo service iofog-controller start 2>/dev/null || sudo /etc/init.d/iofog-controller start\n fi\n ;;\n s6)\n sudo mkdir -p /etc/s6/sv/iofog-controller\n printf '#!/bin/sh\\nexec %s\\n' \"$RUN_CMD_FG\" | sudo tee /etc/s6/sv/iofog-controller/run > /dev/null\n sudo chmod +x /etc/s6/sv/iofog-controller/run\n [ -d /etc/s6/adminsv/default ] && sudo ln -sf /etc/s6/sv/iofog-controller /etc/s6/adminsv/default/iofog-controller 2>/dev/null || true\n sudo s6-svc -u /etc/s6/sv/iofog-controller 2>/dev/null || true\n ;;\n runit)\n sudo mkdir -p /etc/runit/sv/iofog-controller\n printf '#!/bin/sh\\nexec %s\\n' \"$RUN_CMD_FG\" | sudo tee /etc/runit/sv/iofog-controller/run > /dev/null\n sudo chmod +x /etc/runit/sv/iofog-controller/run\n [ -d /var/service ] && sudo ln -sf /etc/runit/sv/iofog-controller /var/service/iofog-controller 2>/dev/null || true\n [ -d /etc/runit/runsvdir/default ] && sudo ln -sf /etc/runit/sv/iofog-controller /etc/runit/runsvdir/default/iofog-controller 2>/dev/null || true\n sudo sv start iofog-controller 2>/dev/null || true\n ;;\n upstart)\n printf 'description \"IoFog Controller container\"\\nstart on runlevel [2345]\\nstop on runlevel [!2345]\\nrespawn\\nrespawn limit 10 5\\nexec %s\\n' \"$RUN_CMD_FG\" | sudo tee /etc/init/iofog-controller.conf > /dev/null\n sudo initctl reload-configuration 2>/dev/null || true\n sudo initctl start iofog-controller 2>/dev/null || true\n ;;\n *)\n sudo tee /etc/init.d/iofog-controller > /dev/null < /dev/null\n#!/bin/sh\nCONTAINER_NAME=\"iofog-controller\"\nif ! podman ps --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Error: The iofog-controller container is not running.\"\n exit 1\nfi\nexec podman exec ${CONTAINER_NAME} iofog-controller \"$@\"\nEOF\n else\n cat <<'EOF' | sudo tee ${EXECUTABLE_FILE} > /dev/null\n#!/bin/sh\nCONTAINER_NAME=\"iofog-controller\"\nif ! docker ps --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Error: The iofog-controller container is not running.\"\n exit 1\nfi\nexec docker exec ${CONTAINER_NAME} iofog-controller \"$@\"\nEOF\n fi\n sudo chmod +x ${EXECUTABLE_FILE}\n\n echo \"ioFog controller installation completed!\"\n}\n\n# main\ncontroller_image=\"$1\"\n\n. /etc/iofog/controller/init.sh\ninit\ndo_stop_iofog_controller\ndo_install_iofog_controller\n\n\n\n"), - } - filem := &embedded.EmbeddedFile{ - Filename: "airgap-controller/set_env.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nETC_DIR=\"/etc/iofog/controller\"\nENV_FILE_NAME=iofog-controller.env # Used as an env file in systemd\n\nENV_FILE=\"$ETC_DIR/$ENV_FILE_NAME\"\n\n# Create folder\nmkdir -p \"$ETC_DIR\"\n\n# Env file (for systemd)\nrm -f \"$ENV_FILE\"\ntouch \"$ENV_FILE\"\n\nfor var in \"$@\"\ndo\n echo \"$var\" >> \"$ENV_FILE\"\ndone"), - } - filen := &embedded.EmbeddedFile{ - Filename: "airgap-controller/uninstall_iofog.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\n\nCONTROLLER_LOG_DIR=\"iofog-controller-log\"\nCONTAINER_NAME=\"iofog-controller\"\nEXECUTABLE_FILE=/usr/local/bin/iofog-controller\nCONTROLLER_DB=iofog-controller-db\n\n\ndo_uninstall_controller() {\n echo \"# Removing ioFog controller...\"\n\n case \"$lsb_dist\" in\n rhel|fedora|centos|ol|sles|opensuse*) CONTAINER_RUNTIME=\"podman\" ;;\n *) CONTAINER_RUNTIME=\"docker\" ;;\n esac\n\n case \"${INIT_SYSTEM:-systemd}\" in\n systemd)\n for f in /etc/systemd/system/iofog-controller.service /etc/containers/systemd/iofog-controller.container; do\n if [ -f \"$f\" ]; then\n echo \"Disabling and stopping systemd service...\"\n sudo systemctl stop iofog-controller.service 2>/dev/null || true\n sudo systemctl disable iofog-controller.service 2>/dev/null || true\n sudo rm -f \"$f\"\n sudo systemctl daemon-reload\n break\n fi\n done\n ;;\n sysvinit|openrc)\n if [ -f /etc/init.d/iofog-controller ]; then\n sudo service iofog-controller stop 2>/dev/null || sudo /etc/init.d/iofog-controller stop 2>/dev/null || true\n [ \"$INIT_SYSTEM\" = \"openrc\" ] && sudo rc-update del iofog-controller default 2>/dev/null || true\n sudo update-rc.d -f iofog-controller remove 2>/dev/null || sudo chkconfig --del iofog-controller 2>/dev/null || true\n sudo rm -f /etc/init.d/iofog-controller\n fi\n ;;\n s6)\n sudo s6-svc -d /etc/s6/sv/iofog-controller 2>/dev/null || true\n sudo rm -rf /etc/s6/sv/iofog-controller\n [ -L /etc/s6/adminsv/default/iofog-controller ] && sudo rm -f /etc/s6/adminsv/default/iofog-controller\n ;;\n runit)\n sudo sv stop iofog-controller 2>/dev/null || true\n [ -L /var/service/iofog-controller ] && sudo rm -f /var/service/iofog-controller\n [ -L /etc/runit/runsvdir/default/iofog-controller ] && sudo rm -f /etc/runit/runsvdir/default/iofog-controller\n sudo rm -rf /etc/runit/sv/iofog-controller\n ;;\n upstart)\n sudo initctl stop iofog-controller 2>/dev/null || true\n sudo rm -f /etc/init/iofog-controller.conf\n ;;\n *)\n sudo systemctl stop iofog-controller 2>/dev/null || true\n sudo systemctl disable iofog-controller 2>/dev/null || true\n sudo rm -f /etc/systemd/system/iofog-controller.service /etc/containers/systemd/iofog-controller.container\n sudo systemctl daemon-reload 2>/dev/null || true\n [ -f /etc/init.d/iofog-controller ] && sudo /etc/init.d/iofog-controller stop 2>/dev/null || true\n sudo rm -f /etc/init.d/iofog-controller\n ;;\n esac\n\n if sudo ${CONTAINER_RUNTIME} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Stopping and removing the ioFog controller container...\"\n sudo ${CONTAINER_RUNTIME} stop ${CONTAINER_NAME} 2>/dev/null || true\n sudo ${CONTAINER_RUNTIME} rm ${CONTAINER_NAME} 2>/dev/null || true\n fi\n\n # Remove config files\n echo \"Checking if the ${CONTAINER_RUNTIME} volume exists...\"\n\n if sudo ${CONTAINER_RUNTIME} volume inspect \"${CONTROLLER_DB}\" >/dev/null 2>&1; then\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' found. Removing...\"\n sudo ${CONTAINER_RUNTIME} volume rm \"${CONTROLLER_DB}\"\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' has been removed.\"\n else\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' does not exist. Skipping removal.\"\n fi\n\n # Remove log files\n echo \"Removing log files...\"\n if sudo ${CONTAINER_RUNTIME} volume inspect \"${CONTROLLER_LOG_DIR}\" >/dev/null 2>&1; then\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' found. Removing...\"\n sudo ${CONTAINER_RUNTIME} volume rm \"${CONTROLLER_LOG_DIR}\"\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' has been removed.\"\n else\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' does not exist. Skipping removal.\"\n fi\n\n\n # Remove the executable script\n if [ -f ${EXECUTABLE_FILE} ]; then\n echo \"Removing the iofog-controller executable script...\"\n sudo rm -f ${EXECUTABLE_FILE}\n fi\n\n echo \"ioFog controller uninstalled successfully!\"\n}\n\n. /etc/iofog/controller/init.sh\ninit\n\ndo_uninstall_controller"), - } - filep := &embedded.EmbeddedFile{ - Filename: "container-agent/check_prereqs.sh", - FileModTime: time.Unix(1775031349, 0), - - Content: string("#!/bin/sh\nset -x\n\n# Check can sudo without password\nif ! $(sudo ls /tmp/ > /dev/null); then\n\tMSG=\"Unable to successfully use sudo with user $USER on this host.\\nUser $USER must be in sudoers group and using sudo without password must be enabled.\\nPlease see iofog.org documentation for more details.\"\n\techo $MSG\n\texit 1\nfi"), - } - fileq := &embedded.EmbeddedFile{ - Filename: "container-agent/init.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\n# Script to detect Linux distribution and version\n# Used as a precursor for system-specific installations\n\n# Exit on error and print commands for debugging\nset -e\nset -x\n\n# Define user variable\nuser=\"$(id -un 2>/dev/null || true)\"\n\n# Check if a command exists\ncommand_exists() {\n command -v \"$@\" > /dev/null 2>&1\n}\n\n# Detect the Linux distribution\nget_distribution() {\n lsb_dist=\"\"\n dist_version=\"\"\n \n # Every system that we officially support has /etc/os-release\n if [ -r /etc/os-release ]; then\n \n lsb_dist=\"$(. /etc/os-release && echo \"$ID\")\"\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n lsb_dist=\"$(echo \"$lsb_dist\" | tr '[:upper:]' '[:lower:]')\"\n else\n echo \"Error: Unsupported Linux distribution! /etc/os-release not found.\"\n exit 1\n fi\n \n echo \"# Detected distribution: $lsb_dist (version: $dist_version)\"\n}\n\n# Check if this is a forked Linux distro\ncheck_forked() {\n # Skip if lsb_release doesn't exist\n if ! command_exists lsb_release; then\n return\n fi\n \n # Check if the `-u` option is supported\n set +e\n lsb_release -a > /dev/null 2>&1\n lsb_release_exit_code=$?\n set -e\n\n # Check if the command has exited successfully, it means we're in a forked distro\n if [ \"$lsb_release_exit_code\" = \"0\" ]; then\n # Get the upstream release info\n current_lsb_dist=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]')\n current_dist_version=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]')\n\n # Print info about current distro\n echo \"You're using '$current_lsb_dist' version '$current_dist_version'.\"\n \n # Check if current is different from detected (indicating a fork)\n if [ \"$current_lsb_dist\" != \"$lsb_dist\" ] || [ \"$current_dist_version\" != \"$dist_version\" ]; then\n echo \"Upstream release is '$lsb_dist' version '$dist_version'.\"\n fi\n else\n # Additional checks for specific distros that might not be properly detected\n if [ -r /etc/debian_version ] && [ \"$lsb_dist\" != \"ubuntu\" ] && [ \"$lsb_dist\" != \"raspbian\" ]; then\n if [ \"$lsb_dist\" = \"osmc\" ]; then\n # OSMC runs Raspbian\n lsb_dist=raspbian\n else\n # We're Debian and don't even know it!\n lsb_dist=debian\n fi\n # Get Debian version and map it to codename\n dist_version=\"$(sed 's/\\/.*//' /etc/debian_version | sed 's/\\..*//')\"\n case \"$dist_version\" in\n 14)\n dist_version=\"forky\"\n ;;\n 13)\n dist_version=\"trixie\"\n ;;\n 12)\n dist_version=\"bookworm\"\n ;;\n 11)\n dist_version=\"bullseye\"\n ;;\n 10)\n dist_version=\"buster\"\n ;;\n 9)\n dist_version=\"stretch\"\n ;;\n 8|'Kali Linux 2')\n dist_version=\"jessie\"\n ;;\n 7)\n dist_version=\"wheezy\"\n ;;\n esac\n elif [ -r /etc/redhat-release ] && [ -z \"$lsb_dist\" ]; then\n lsb_dist=redhat\n # Extract version from redhat-release file\n dist_version=\"$(sed 's/.*release \\([0-9.]*\\).*/\\1/' /etc/redhat-release)\"\n fi\n fi\n}\n\n# Set up sudo command if necessary\nsetup_sudo() {\n sh_c='sh -c'\n if [ \"$user\" != 'root' ]; then\n if command_exists sudo; then\n sh_c='sudo -E sh -c'\n elif command_exists su; then\n sh_c='su -c'\n else\n echo \"Error: this installer needs the ability to run commands as root.\"\n echo \"We are unable to find either 'sudo' or 'su' available to make this happen.\"\n exit 1\n fi\n fi\n echo \"# Using command executor: $sh_c\"\n}\n\n# Refine distribution version detection based on the distro\nrefine_distribution_version() {\n case \"$lsb_dist\" in\n ubuntu)\n if command_exists lsb_release; then\n dist_version=\"$(lsb_release --codename | cut -f2)\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/lsb-release ]; then\n \n dist_version=\"$(. /etc/lsb-release && echo \"$DISTRIB_CODENAME\")\"\n fi\n ;;\n\n debian|raspbian)\n # If we only have a number, map it to a codename for better recognition\n if echo \"$dist_version\" | grep -qE '^[0-9]+$'; then\n case \"$dist_version\" in\n 14)\n dist_version=\"forky\"\n ;;\n 13)\n dist_version=\"trixie\"\n ;;\n 12)\n dist_version=\"bookworm\"\n ;;\n 11)\n dist_version=\"bullseye\"\n ;;\n 10)\n # Handle special case for Buster\n dist_version=\"buster\"\n if [ \"$user\" = 'root' ]; then\n apt-get update --allow-releaseinfo-change || true\n elif command_exists sudo; then\n sudo apt-get update --allow-releaseinfo-change || true\n fi\n ;;\n 9)\n dist_version=\"stretch\"\n ;;\n 8)\n dist_version=\"jessie\"\n ;;\n 7)\n dist_version=\"wheezy\"\n ;;\n esac\n fi\n ;;\n\n centos|rhel|fedora|ol)\n # Make sure we have a version number\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/redhat-release ]; then\n dist_version=\"$(sed 's/.*release \\([0-9.]*\\).*/\\1/' /etc/redhat-release)\"\n fi\n ;;\n\n sles|opensuse)\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n # Fallback for older versions\n if [ -z \"$dist_version\" ] && [ -r /etc/SuSE-release ]; then\n dist_version=\"$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = //')\"\n fi\n # Ensure version is in the correct format (e.g., 15.4 for SLES 15 SP4)\n if [ -n \"$dist_version\" ]; then\n # Remove any non-numeric characters except dots\n dist_version=\"$(echo \"$dist_version\" | sed 's/[^0-9.]//g')\"\n fi\n # Normalize distribution name\n if [ \"$lsb_dist\" = \"sles\" ]; then\n lsb_dist=\"sles\"\n elif [ \"$lsb_dist\" = \"opensuse\" ]; then\n lsb_dist=\"opensuse\"\n fi\n ;;\n\n *)\n if command_exists lsb_release; then\n dist_version=\"$(lsb_release --release | cut -f2)\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n ;;\n esac\n}\n\n# Detect init system \ndetect_init_system() {\n if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then\n INIT_SYSTEM=\"systemd\"\n elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then\n INIT_SYSTEM=\"upstart\"\n elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then\n INIT_SYSTEM=\"openrc\"\n elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then\n INIT_SYSTEM=\"s6\"\n elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then\n INIT_SYSTEM=\"runit\"\n elif [ -d /etc/init.d ]; then\n INIT_SYSTEM=\"sysvinit\"\n else\n INIT_SYSTEM=\"unknown\"\n fi\n export INIT_SYSTEM\n echo \"# Detected init system: $INIT_SYSTEM\"\n}\n\n# Detect package type (deb, rpm, or other)\ndetect_package_type() {\n case \"$lsb_dist\" in\n debian|ubuntu|raspbian|mendel)\n PACKAGE_TYPE=\"deb\"\n ;;\n fedora|centos|rhel|ol|sles|opensuse*)\n PACKAGE_TYPE=\"rpm\"\n ;;\n alpine)\n PACKAGE_TYPE=\"apk\"\n ;;\n *)\n if command_exists apt-get || command_exists dpkg; then\n PACKAGE_TYPE=\"deb\"\n elif command_exists yum || command_exists dnf || command_exists zypper; then\n PACKAGE_TYPE=\"rpm\"\n elif command_exists apk; then\n PACKAGE_TYPE=\"apk\"\n else\n PACKAGE_TYPE=\"other\"\n fi\n ;;\n esac\n export PACKAGE_TYPE\n echo \"# Detected package type: $PACKAGE_TYPE\"\n}\n\n# Init function\ninit() {\n # Detect basic distribution info\n get_distribution\n \n # Set up sudo for privileged commands\n setup_sudo\n \n # Refine version information\n refine_distribution_version\n \n # Check if this is a forked distro\n check_forked\n \n # Detect init system and package type (for universal OS/init support)\n detect_init_system\n detect_package_type\n \n # Print final distribution information\n echo \"----------------------------------------\"\n echo \"Linux Distribution: $lsb_dist\"\n echo \"Version: $dist_version\"\n echo \"Init system: $INIT_SYSTEM\"\n echo \"Package type: $PACKAGE_TYPE\"\n echo \"----------------------------------------\"\n \n}\n"), - } - filer := &embedded.EmbeddedFile{ - Filename: "container-agent/install_container_engine.sh", - FileModTime: time.Unix(1775031471, 0), - - Content: string("#!/bin/sh\n# Script to install Docker/Podman based on Linux distribution\n# Sources init.sh for distribution detection\n\nset -x\nset -e\n\nCONTAINER_ENGINE_MSG=\"This operating system does not support automatic container engine installation. Please install Docker 25+ or Podman 4+ on the target host and re-run, or use an airgap deployment with a pre-installed engine.\"\n\ncheck_docker_version() {\n docker_version_num=0\n if command -v docker >/dev/null 2>&1; then\n raw=$(docker -v 2>/dev/null | sed 's/.*version \\([^,]*\\),.*/\\1/' | tr -d '.')\n [ -n \"$raw\" ] && docker_version_num=\"$raw\"\n fi\n [ \"$docker_version_num\" -ge 2500 ] 2>/dev/null || return 1\n}\n\ncheck_podman_version() {\n podman_version_num=0\n if command -v podman >/dev/null 2>&1; then\n raw=$(podman --version 2>/dev/null | sed -n 's/.*version \\([0-9][0-9]*\\).*/\\1/p')\n [ -n \"$raw\" ] && podman_version_num=\"$raw\"\n fi\n [ \"$podman_version_num\" -ge 4 ] 2>/dev/null || return 1\n}\n\nstart_docker() {\n set +e\n if $sh_c \"docker ps\" >/dev/null 2>&1; then\n set -e\n return 0\n fi\n err_code=1\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl start docker\" >/dev/null 2>&1\n err_code=$?\n ;;\n sysvinit)\n $sh_c \"service docker start\" >/dev/null 2>&1 || $sh_c \"/etc/init.d/docker start\" >/dev/null 2>&1\n err_code=$?\n ;;\n openrc)\n $sh_c \"rc-service docker start\" >/dev/null 2>&1\n err_code=$?\n ;;\n *)\n $sh_c \"/etc/init.d/docker start\" >/dev/null 2>&1\n err_code=$?\n [ $err_code -ne 0 ] && $sh_c \"systemctl start docker\" >/dev/null 2>&1 && err_code=0\n [ $err_code -ne 0 ] && $sh_c \"service docker start\" >/dev/null 2>&1 && err_code=0\n [ $err_code -ne 0 ] && $sh_c \"snap start docker\" >/dev/null 2>&1 && err_code=0\n ;;\n esac\n set -e\n if [ $err_code -ne 0 ]; then\n echo \"Could not start Docker daemon\"\n exit 1\n fi\n}\n\nstart_podman() {\n set +e\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl start podman\" >/dev/null 2>&1\n $sh_c \"systemctl start podman.socket\" >/dev/null 2>&1\n ;;\n sysvinit)\n $sh_c \"service podman start\" >/dev/null 2>&1 || $sh_c \"/etc/init.d/podman start\" >/dev/null 2>&1\n ;;\n openrc)\n $sh_c \"rc-service podman start\" >/dev/null 2>&1\n ;;\n *)\n $sh_c \"systemctl start podman\" >/dev/null 2>&1 || true\n $sh_c \"systemctl start podman.socket\" >/dev/null 2>&1 || true\n $sh_c \"service podman start\" >/dev/null 2>&1 || true\n ;;\n esac\n set -e\n}\n\n\ndo_modify_daemon() {\n # Skip for Podman installations\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"# Configuring Podman for CDI directory support...\"\n\n # Create CDI directories\n $sh_c \"mkdir -p /etc/cdi /var/run/cdi\"\n\n # Ensure /etc/containers exists\n $sh_c \"mkdir -p /etc/containers\"\n\n # Create containers.conf if it doesn't exist\n if [ ! -f \"/etc/containers/containers.conf\" ]; then\n $sh_c 'cat > /etc/containers/containers.conf <> /etc/containers/containers.conf'\n fi\n fi\n\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl enable podman\" 2>/dev/null || true\n $sh_c \"systemctl enable podman.socket\" 2>/dev/null || true\n ;;\n openrc)\n $sh_c \"rc-update add podman default\" 2>/dev/null || true\n ;;\n sysvinit)\n $sh_c \"update-rc.d podman defaults\" 2>/dev/null || $sh_c \"chkconfig podman on\" 2>/dev/null || true\n ;;\n *) ;;\n esac\n start_podman\n return\n fi\n\n # Original Docker daemon configuration\n if [ ! -f /etc/docker/daemon.json ]; then\n echo \"Creating /etc/docker/daemon.json...\"\n $sh_c \"mkdir -p /etc/docker\"\n $sh_c 'cat > /etc/docker/daemon.json << EOF\n{\n\t\"storage-driver\": \"overlayfs\",\n \"features\": {\n \"containerd-snapshotter\": true,\n \"cdi\": true\n },\n \"cdi-spec-dirs\": [\"/etc/cdi/\", \"/var/run/cdi\"]\n}\nEOF'\n else\n echo \"/etc/docker/daemon.json already exists\"\n fi\n echo \"Restarting Docker daemon...\"\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl daemon-reload\"\n $sh_c \"systemctl restart docker\"\n ;;\n *)\n $sh_c \"systemctl daemon-reload\" 2>/dev/null || true\n $sh_c \"systemctl restart docker\" 2>/dev/null || start_docker\n ;;\n esac\n}\n\ndo_install_container_engine() {\n if [ \"$PACKAGE_TYPE\" = \"apk\" ]; then\n if command_exists docker && check_docker_version; then\n echo \"# Docker already installed (>= 25)\"\n start_docker\n do_modify_daemon\n return 0\n fi\n echo \"# Installing Docker on Alpine...\"\n $sh_c \"apk add docker\"\n $sh_c \"rc-update add docker default\"\n $sh_c \"service docker start\"\n $sh_c \"addgroup $user docker\"\n if ! command_exists docker; then\n echo \"Failed to install Docker\"\n exit 1\n fi\n if ! check_docker_version; then\n echo \"Error: Docker 25+ is required. Please upgrade the Docker package or install Docker 25+ manually.\"\n exit 1\n fi\n start_docker\n do_modify_daemon\n return 0\n fi\n\n if [ \"$PACKAGE_TYPE\" = \"other\" ]; then\n if check_docker_version; then\n USE_PODMAN=\"false\"\n echo \"# Docker (>= 25) found; using Docker.\"\n start_docker\n do_modify_daemon\n return 0\n fi\n if check_podman_version; then\n USE_PODMAN=\"true\"\n echo \"# Podman (>= 4) found; using Podman.\"\n do_modify_daemon\n return 0\n fi\n echo \"Error: $CONTAINER_ENGINE_MSG\"\n exit 1\n fi\n\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"# Installing Podman and related packages...\"\n case \"$lsb_dist\" in\n fedora|centos|rhel|ol)\n $sh_c \"yum install -y podman crun podman-docker\"\n ;;\n sles|opensuse*)\n $sh_c \"zypper install -y podman crun podman-docker\"\n ;;\n esac\n if ! check_podman_version; then\n echo \"Error: Podman 4+ is required. Please upgrade Podman.\"\n exit 1\n fi\n do_modify_daemon\n return\n fi\n\n if command_exists docker; then\n docker_version=$(docker -v 2>/dev/null | sed 's/.*version \\([^,]*\\),.*/\\1/' | tr -d '.')\n if [ -n \"$docker_version\" ] && [ \"$docker_version\" -ge 2500 ] 2>/dev/null; then\n echo \"# Docker already installed (>= 25)\"\n start_docker\n do_modify_daemon\n return\n fi\n fi\n\n echo \"# Installing Docker...\"\n case \"$lsb_dist\" in\n debian|ubuntu|raspbian)\n case \"$dist_version\" in\n \"stretch\")\n $sh_c \"apt install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common\"\n curl -fsSL https://download.docker.com/linux/debian/gpg | $sh_c \"apt-key add -\"\n $sh_c \"add-apt-repository \\\"deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/debian $(lsb_release -cs) stable\\\"\"\n $sh_c \"apt update -y\"\n $sh_c \"apt install -y docker-ce\"\n ;;\n *)\n curl -fsSL https://get.docker.com/ | $sh_c \"sh\"\n ;;\n esac\n ;;\n *)\n curl -fsSL https://get.docker.com/ | $sh_c \"sh\"\n ;;\n esac\n\n if ! command_exists docker; then\n echo \"Failed to install Docker\"\n exit 1\n fi\n if ! check_docker_version; then\n echo \"Error: Docker 25+ is required. Please upgrade Docker.\"\n exit 1\n fi\n start_docker\n do_modify_daemon\n}\n\n# Check if we should use Podman based on distribution\ndetermine_container_engine() {\n USE_PODMAN=\"false\"\n case \"$lsb_dist\" in\n fedora|centos|rhel|ol|sles|opensuse*)\n USE_PODMAN=\"true\"\n echo \"# Using Podman for $lsb_dist\"\n ;;\n *)\n echo \"# Using Docker for $lsb_dist\"\n ;;\n esac\n}\n\n# Source init.sh to get distribution info\n. /etc/iofog/agent/init.sh\ninit\n\n# Configure container engine based on distribution\ndetermine_container_engine\n\n# Install appropriate container engine\ndo_install_container_engine\n\necho \"# Installation completed successfully\""), - } - files := &embedded.EmbeddedFile{ - Filename: "container-agent/install_deps.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\n\n/etc/iofog/agent/install_container_engine.sh\n"), - } - filet := &embedded.EmbeddedFile{ - Filename: "container-agent/install_iofog.sh", - FileModTime: time.Unix(1775031617, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nAGENT_LOG_FOLDER=/var/log/iofog-agent\nAGENT_BACKUP_FOLDER=/var/backups/iofog-agent\nAGENT_MESSAGE_FOLDER=/var/lib/iofog-agent\nAGENT_SHARE_FOLDER=/usr/share/iofog-agent\nSAVED_AGENT_CONFIG_FOLDER=/tmp/agent-config-save\nAGENT_CONTAINER_NAME=\"iofog-agent\"\nETC_DIR=/etc/iofog/agent\n\ndo_check_install() {\n\tif command_exists iofog-agent; then\n\t\tlocal VERSION=$(sudo iofog-agent version | head -n1 | sed \"s/ioFog//g\" | tr -d ' ' | tr -d \"\\n\")\n\t\tif [ \"$VERSION\" = \"$agent_version\" ]; then\n\t\t\techo \"Agent $VERSION already installed.\"\n\t\t\texit 0\n\t\tfi\n\tfi\n}\n\ndo_stop_iofog() {\n\tif ! command_exists iofog-agent; then\n\t\treturn 0\n\tfi\n\tcase \"${INIT_SYSTEM:-systemd}\" in\n\t\tsystemd)\n\t\t\tsudo systemctl stop iofog-agent 2>/dev/null || true\n\t\t\t;;\n\t\tsysvinit|openrc)\n\t\t\tsudo service iofog-agent stop 2>/dev/null || sudo /etc/init.d/iofog-agent stop 2>/dev/null || true\n\t\t\t;;\n\t\ts6)\n\t\t\tsudo s6-svc -d /etc/s6/sv/iofog-agent 2>/dev/null || true\n\t\t\t;;\n\t\trunit)\n\t\t\tsudo sv stop iofog-agent 2>/dev/null || true\n\t\t\t;;\n\t\tupstart)\n\t\t\tsudo initctl stop iofog-agent 2>/dev/null || true\n\t\t\t;;\n\t\t*)\n\t\t\tsudo systemctl stop iofog-agent 2>/dev/null || sudo service iofog-agent stop 2>/dev/null || true\n\t\t\t;;\n\tesac\n\t# Ensure container is stopped by name (in case init did not)\n\t(docker stop ${AGENT_CONTAINER_NAME} 2>/dev/null || podman stop ${AGENT_CONTAINER_NAME} 2>/dev/null) || true\n}\n\n\n\ndo_create_env() {\nENV_FILE_NAME=iofog-agent.env # Used as an env file in systemd\n\nENV_FILE=\"$ETC_DIR/$ENV_FILE_NAME\"\n\n# Env file (for systemd)\nrm -f \"$ENV_FILE\"\ntouch \"$ENV_FILE\"\n\necho \"IOFOG_AGENT_IMAGE=${agent_image}\" >> \"$ENV_FILE\"\necho \"IOFOG_AGENT_TZ=${agent_tz}\" >> \"$ENV_FILE\"\n\n}\n\ndo_install_iofog() {\n\techo \"# Installing ioFog agent...\"\n\t\n # 1. Ensure folders exist\n for FOLDER in ${ETC_DIR} ${AGENT_LOG_FOLDER} ${AGENT_BACKUP_FOLDER} ${AGENT_MESSAGE_FOLDER} ${AGENT_SHARE_FOLDER}; do\n if [ ! -d \"$FOLDER\" ]; then\n echo \"Creating folder: $FOLDER\"\n sudo mkdir -p \"$FOLDER\"\n sudo chmod 775 \"$FOLDER\"\n fi\n done\n\tdo_create_env\n\n # Determine container engine (Podman for rpm-like distros, else Docker)\n USE_PODMAN=\"false\"\n case \"$lsb_dist\" in\n rhel|centos|fedora|ol|sles|opensuse*) USE_PODMAN=\"true\" ;;\n esac\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n CONTAINER_RUNTIME=\"podman\"\n SOCK_MOUNT=\"-v /run/podman/podman.sock:/run/podman/podman.sock:rw\"\n else\n CONTAINER_RUNTIME=\"docker\"\n SOCK_MOUNT=\"-v /var/run/docker.sock:/var/run/docker.sock:rw\"\n fi\n\n # Systemd: use Quadlet for Podman or systemd unit for Docker\n if [ \"${INIT_SYSTEM:-systemd}\" = \"systemd\" ]; then\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"Using Podman (Quadlet) for container management...\"\n SYSTEMD_SERVICE_FILE=/etc/containers/systemd/iofog-agent.container\n cat < /dev/null\n[Unit]\nDescription=ioFog Agent Service\nAfter=podman.service\nRequires=podman.service\n\n[Container]\nContainerName=${AGENT_CONTAINER_NAME}\nImage=${agent_image}\nPodmanArgs=--privileged --stop-timeout=60\nEnvironmentFile=${ETC_DIR}/iofog-agent.env\nNetwork=host\nVolume=/run/podman/podman.sock:/run/podman/podman.sock:rw\nVolume=iofog-agent-config:/etc/iofog-agent:rw\nVolume=/var/log/iofog-agent:/var/log/iofog-agent:rw\nVolume=/var/backups/iofog-agent:/var/backups/iofog-agent:rw\nVolume=/usr/share/iofog-agent:/usr/share/iofog-agent:rw\nVolume=/var/lib/iofog-agent:/var/lib/iofog-agent:rw\nLogDriver=journald\n\n[Service]\nRestart=always\n\n[Install]\nWantedBy=default.target\nEOF\n sudo systemctl daemon-reload\n sudo systemctl restart podman 2>/dev/null || true\n sudo systemctl enable iofog-agent.service\n sudo systemctl start iofog-agent.service\n else\n echo \"Using Docker (systemd) for container management...\"\n SYSTEMD_SERVICE_FILE=/etc/systemd/system/iofog-agent.service\n cat < /dev/null\n[Unit]\nDescription=ioFog Agent Service\nAfter=docker.service\nRequires=docker.service\n\n[Service]\nRestart=always\nExecStartPre=-/usr/bin/docker rm -f ${AGENT_CONTAINER_NAME}\nExecStart=/usr/bin/docker run --rm --name ${AGENT_CONTAINER_NAME} \\\\\n--env-file ${ETC_DIR}/iofog-agent.env \\\\\n-v /var/run/docker.sock:/var/run/docker.sock:rw \\\\\n-v iofog-agent-config:/etc/iofog-agent:rw \\\\\n-v /var/log/iofog-agent:/var/log/iofog-agent:rw \\\\\n-v /var/backups/iofog-agent:/var/backups/iofog-agent:rw \\\\\n-v /usr/share/iofog-agent:/usr/share/iofog-agent:rw \\\\\n-v /var/lib/iofog-agent:/var/lib/iofog-agent:rw \\\\\n--net=host \\\\\n--privileged \\\\\n--stop-timeout 60 \\\\\n--attach stdout \\\\\n--attach stderr \\\\\n${agent_image}\nExecStop=/usr/bin/docker stop ${AGENT_CONTAINER_NAME}\n\n[Install]\nWantedBy=default.target\nEOF\n sudo systemctl daemon-reload\n sudo systemctl enable iofog-agent.service\n sudo systemctl start iofog-agent.service\n fi\n else\n # Non-systemd: create init script that runs the container\n echo \"Using $CONTAINER_RUNTIME with $INIT_SYSTEM for container management...\"\n RUN_CMD=\"${CONTAINER_RUNTIME} run --rm -d --name ${AGENT_CONTAINER_NAME} --env-file ${ETC_DIR}/iofog-agent.env ${SOCK_MOUNT} -v iofog-agent-config:/etc/iofog-agent:rw -v /var/log/iofog-agent:/var/log/iofog-agent:rw -v /var/backups/iofog-agent:/var/backups/iofog-agent:rw -v /usr/share/iofog-agent:/usr/share/iofog-agent:rw -v /var/lib/iofog-agent:/var/lib/iofog-agent:rw --net=host --privileged --stop-timeout 60 ${agent_image}\"\n RUN_CMD_FG=\"${CONTAINER_RUNTIME} run --rm --name ${AGENT_CONTAINER_NAME} --env-file ${ETC_DIR}/iofog-agent.env ${SOCK_MOUNT} -v iofog-agent-config:/etc/iofog-agent:rw -v /var/log/iofog-agent:/var/log/iofog-agent:rw -v /var/backups/iofog-agent:/var/backups/iofog-agent:rw -v /usr/share/iofog-agent:/usr/share/iofog-agent:rw -v /var/lib/iofog-agent:/var/lib/iofog-agent:rw --net=host --privileged --stop-timeout 60 ${agent_image}\"\n STOP_CMD=\"${CONTAINER_RUNTIME} stop ${AGENT_CONTAINER_NAME}\"\n\n case \"$INIT_SYSTEM\" in\n sysvinit|openrc)\n sudo tee /etc/init.d/iofog-agent > /dev/null </dev/null | grep -q \"^${AGENT_CONTAINER_NAME}\\$\"; then exit 0; fi\n $RUN_CMD\n ;;\n stop)\n $STOP_CMD 2>/dev/null || true\n ;;\n restart)\n \\$0 stop; \\$0 start\n ;;\n status)\n if ${CONTAINER_RUNTIME} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^${AGENT_CONTAINER_NAME}\\$\"; then echo \"running\"; exit 0; else echo \"stopped\"; exit 1; fi\n ;;\n *)\n echo \"Usage: \\$0 {start|stop|restart|status}\"\n exit 1\n ;;\nesac\nexit 0\nINITSCRIPT\n sudo chmod +x /etc/init.d/iofog-agent\n if [ \"$INIT_SYSTEM\" = \"openrc\" ]; then\n sudo rc-update add iofog-agent default 2>/dev/null || true\n sudo rc-service iofog-agent start\n else\n sudo update-rc.d iofog-agent defaults 2>/dev/null || sudo chkconfig iofog-agent on 2>/dev/null || true\n sudo service iofog-agent start 2>/dev/null || sudo /etc/init.d/iofog-agent start\n fi\n ;;\n s6)\n sudo mkdir -p /etc/s6/sv/iofog-agent\n sudo tee /etc/s6/sv/iofog-agent/run > /dev/null </dev/null || true\n sudo s6-svc -u /etc/s6/sv/iofog-agent 2>/dev/null || true\n ;;\n runit)\n sudo mkdir -p /etc/runit/sv/iofog-agent\n sudo tee /etc/runit/sv/iofog-agent/run > /dev/null </dev/null || true\n elif [ -d /etc/runit/runsvdir/default ]; then\n sudo ln -sf /etc/runit/sv/iofog-agent /etc/runit/runsvdir/default/iofog-agent 2>/dev/null || true\n fi\n sudo sv start iofog-agent 2>/dev/null || true\n ;;\n upstart)\n sudo tee /etc/init/iofog-agent.conf > /dev/null </dev/null || true\n sudo initctl start iofog-agent 2>/dev/null || true\n ;;\n *)\n echo \"Warning: Unknown init system $INIT_SYSTEM. Creating /etc/init.d/iofog-agent fallback.\"\n sudo tee /etc/init.d/iofog-agent > /dev/null < /dev/null\n#!/bin/sh\nCONTAINER_NAME=\"iofog-agent\"\nif ! podman ps --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Error: The iofog-agent container is not running.\"\n exit 1\nfi\nexec podman exec ${CONTAINER_NAME} iofog-agent \"$@\"\nEOF\n else\n cat <<'EOF' | sudo tee ${EXECUTABLE_FILE} > /dev/null\n#!/bin/sh\nCONTAINER_NAME=\"iofog-agent\"\nif ! docker ps --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Error: The iofog-agent container is not running.\"\n exit 1\nfi\nexec docker exec ${CONTAINER_NAME} iofog-agent \"$@\"\nEOF\n fi\n sudo chmod +x ${EXECUTABLE_FILE}\n\n echo \"ioFog agent installation completed!\"\n}\n\ndo_start_iofog(){\n\tcase \"${INIT_SYSTEM:-systemd}\" in\n\t\tsystemd)\n\t\t\tsudo systemctl start iofog-agent >/dev/null 2>&1 &\n\t\t\t;;\n\t\tsysvinit|openrc)\n\t\t\tsudo service iofog-agent start 2>/dev/null || sudo /etc/init.d/iofog-agent start 2>/dev/null &\n\t\t\t;;\n\t\ts6)\n\t\t\tsudo s6-svc -u /etc/s6/sv/iofog-agent 2>/dev/null &\n\t\t\t;;\n\t\trunit)\n\t\t\tsudo sv start iofog-agent 2>/dev/null &\n\t\t\t;;\n\t\tupstart)\n\t\t\tsudo initctl start iofog-agent 2>/dev/null &\n\t\t\t;;\n\t\t*)\n\t\t\tsudo systemctl start iofog-agent 2>/dev/null || sudo /etc/init.d/iofog-agent start 2>/dev/null &\n\t\t\t;;\n\tesac\n\tlocal STATUS=\"\"\n\tlocal ITER=0\n\twhile [ \"$STATUS\" != \"RUNNING\" ]; do\n\t\tITER=$((ITER+1))\n\t\tif [ \"$ITER\" -gt 600 ]; then\n\t\t\techo \"Timed out waiting for Agent to be RUNNING\"\n\t\t\texit 1\n\t\tfi\n\t\tsleep 1\n\t\tSTATUS=$(sudo iofog-agent status 2>/dev/null | cut -f2 -d: | head -n 1 | tr -d '[:space:]')\n\t\techo \"${STATUS}\"\n\tdone\n\tsudo iofog-agent \"config -cf 10 -sf 10\"\n\tif [ \"$lsb_dist\" = \"rhel\" ] || [ \"$lsb_dist\" = \"centos\" ] || [ \"$lsb_dist\" = \"fedora\" ] || [ \"$lsb_dist\" = \"ol\" ] || [ \"$lsb_dist\" = \"sles\" ] || [ \"$lsb_dist\" = \"opensuse\" ]; then\n\t\tsudo iofog-agent \"config -c unix:///var/run/podman/podman.sock\"\n\tfi\n}\n\nagent_image=\"$1\"\nagent_tz=\"$2\"\necho \"Using variables\"\necho \"version: $agent_image\"\necho \"timezone: $agent_tz\"\n. /etc/iofog/agent/init.sh\ninit\ndo_check_install\ndo_stop_iofog\ndo_install_iofog\ndo_start_iofog"), - } - fileu := &embedded.EmbeddedFile{ - Filename: "container-agent/uninstall_iofog.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nAGENT_CONFIG_FOLDER=iofog-agent-config\nAGENT_LOG_FOLDER=/var/log/iofog-agent\nAGENT_BACKUP_FOLDER=/var/backups/iofog-agent\nAGENT_MESSAGE_FOLDER=/var/lib/iofog-agent\nEXECUTABLE_FILE=/usr/local/bin/iofog-agent\nCONTAINER_NAME=\"iofog-agent\"\n\ndo_uninstall_iofog() {\n echo \"# Removing ioFog agent...\"\n\n case \"$lsb_dist\" in\n rhel|fedora|centos|ol|sles|opensuse*) CONTAINER_RUNTIME=\"podman\" ;;\n *) CONTAINER_RUNTIME=\"docker\" ;;\n esac\n\n # Stop and remove service based on init system\n case \"${INIT_SYSTEM:-systemd}\" in\n systemd)\n for f in /etc/systemd/system/iofog-agent.service /etc/containers/systemd/iofog-agent.container; do\n if [ -f \"$f\" ]; then\n echo \"Disabling and stopping systemd service...\"\n sudo systemctl stop iofog-agent.service 2>/dev/null || true\n sudo systemctl disable iofog-agent.service 2>/dev/null || true\n sudo rm -f \"$f\"\n sudo systemctl daemon-reload\n break\n fi\n done\n ;;\n sysvinit|openrc)\n if [ -f /etc/init.d/iofog-agent ]; then\n sudo service iofog-agent stop 2>/dev/null || sudo /etc/init.d/iofog-agent stop 2>/dev/null || true\n [ \"$INIT_SYSTEM\" = \"openrc\" ] && sudo rc-update del iofog-agent default 2>/dev/null || true\n sudo update-rc.d -f iofog-agent remove 2>/dev/null || sudo chkconfig --del iofog-agent 2>/dev/null || true\n sudo rm -f /etc/init.d/iofog-agent\n fi\n ;;\n s6)\n sudo s6-svc -d /etc/s6/sv/iofog-agent 2>/dev/null || true\n sudo rm -rf /etc/s6/sv/iofog-agent\n [ -L /etc/s6/adminsv/default/iofog-agent ] && sudo rm -f /etc/s6/adminsv/default/iofog-agent\n ;;\n runit)\n sudo sv stop iofog-agent 2>/dev/null || true\n [ -L /var/service/iofog-agent ] && sudo rm -f /var/service/iofog-agent\n [ -L /etc/runit/runsvdir/default/iofog-agent ] && sudo rm -f /etc/runit/runsvdir/default/iofog-agent\n sudo rm -rf /etc/runit/sv/iofog-agent\n ;;\n upstart)\n sudo initctl stop iofog-agent 2>/dev/null || true\n sudo rm -f /etc/init/iofog-agent.conf\n ;;\n *)\n sudo systemctl stop iofog-agent 2>/dev/null || true\n sudo systemctl disable iofog-agent 2>/dev/null || true\n sudo rm -f /etc/systemd/system/iofog-agent.service /etc/containers/systemd/iofog-agent.container\n sudo systemctl daemon-reload 2>/dev/null || true\n [ -f /etc/init.d/iofog-agent ] && sudo /etc/init.d/iofog-agent stop 2>/dev/null || true\n sudo rm -f /etc/init.d/iofog-agent\n ;;\n esac\n\n # Remove the container\n if sudo ${CONTAINER_RUNTIME} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Stopping and removing the ioFog agent container...\"\n sudo ${CONTAINER_RUNTIME} stop ${CONTAINER_NAME} 2>/dev/null || true\n sudo ${CONTAINER_RUNTIME} rm ${CONTAINER_NAME} 2>/dev/null || true\n fi\n\n # Remove config files\n echo \"Checking if the ${CONTAINER_RUNTIME} volume exists...\"\n\n if sudo ${CONTAINER_RUNTIME} volume inspect \"${AGENT_CONFIG_FOLDER}\" >/dev/null 2>&1; then\n echo \"${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' found. Removing...\"\n sudo ${CONTAINER_RUNTIME} volume rm \"${AGENT_CONFIG_FOLDER}\"\n echo \"${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' has been removed.\"\n else\n echo \"${CONTAINER_RUNTIME} volume '${AGENT_CONFIG_FOLDER}' does not exist. Skipping removal.\"\n fi\n\n # Remove log files\n echo \"Removing log files...\"\n sudo rm -rf ${AGENT_LOG_FOLDER}\n\n # Remove backup files\n echo \"Removing backup files...\"\n sudo rm -rf ${AGENT_BACKUP_FOLDER}\n\n # Remove message files\n echo \"Removing message files...\"\n sudo rm -rf ${AGENT_MESSAGE_FOLDER}\n\n # Remove the executable script\n if [ -f ${EXECUTABLE_FILE} ]; then\n echo \"Removing the iofog-agent executable script...\"\n sudo rm -f ${EXECUTABLE_FILE}\n fi\n\n echo \"ioFog agent uninstalled successfully!\"\n}\n\n. /etc/iofog/agent/init.sh\ninit\n\ndo_uninstall_iofog\n"), - } - filew := &embedded.EmbeddedFile{ - Filename: "container-controller/check_prereqs.sh", - FileModTime: time.Unix(1775031353, 0), - - Content: string("#!/bin/sh\nset -x\n\n# Check can sudo without password\nif ! $(sudo ls /tmp/ > /dev/null); then\n\tMSG=\"Unable to successfully use sudo with user $USER on this host.\\nUser $USER must be in sudoers group and using sudo without password must be enabled.\\nPlease see iofog.org documentation for more details.\"\n\techo $MSG\n\texit 1\nfi"), - } - filex := &embedded.EmbeddedFile{ - Filename: "container-controller/init.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\n# Script to detect Linux distribution and version\n# Used as a precursor for system-specific installations\n\n# Exit on error and print commands for debugging\nset -e\nset -x\n\n# Define user variable\nuser=\"$(id -un 2>/dev/null || true)\"\n\n# Check if a command exists\ncommand_exists() {\n command -v \"$@\" > /dev/null 2>&1\n}\n\n# Detect the Linux distribution\nget_distribution() {\n lsb_dist=\"\"\n dist_version=\"\"\n \n # Every system that we officially support has /etc/os-release\n if [ -r /etc/os-release ]; then\n \n lsb_dist=\"$(. /etc/os-release && echo \"$ID\")\"\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n lsb_dist=\"$(echo \"$lsb_dist\" | tr '[:upper:]' '[:lower:]')\"\n else\n echo \"Error: Unsupported Linux distribution! /etc/os-release not found.\"\n exit 1\n fi\n \n echo \"# Detected distribution: $lsb_dist (version: $dist_version)\"\n}\n\n# Check if this is a forked Linux distro\ncheck_forked() {\n # Skip if lsb_release doesn't exist\n if ! command_exists lsb_release; then\n return\n fi\n \n # Check if the `-u` option is supported\n set +e\n lsb_release -a > /dev/null 2>&1\n lsb_release_exit_code=$?\n set -e\n\n # Check if the command has exited successfully, it means we're in a forked distro\n if [ \"$lsb_release_exit_code\" = \"0\" ]; then\n # Get the upstream release info\n current_lsb_dist=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]')\n current_dist_version=$(lsb_release -a 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]')\n\n # Print info about current distro\n echo \"You're using '$current_lsb_dist' version '$current_dist_version'.\"\n \n # Check if current is different from detected (indicating a fork)\n if [ \"$current_lsb_dist\" != \"$lsb_dist\" ] || [ \"$current_dist_version\" != \"$dist_version\" ]; then\n echo \"Upstream release is '$lsb_dist' version '$dist_version'.\"\n fi\n else\n # Additional checks for specific distros that might not be properly detected\n if [ -r /etc/debian_version ] && [ \"$lsb_dist\" != \"ubuntu\" ] && [ \"$lsb_dist\" != \"raspbian\" ]; then\n if [ \"$lsb_dist\" = \"osmc\" ]; then\n # OSMC runs Raspbian\n lsb_dist=raspbian\n else\n # We're Debian and don't even know it!\n lsb_dist=debian\n fi\n # Get Debian version and map it to codename\n dist_version=\"$(sed 's/\\/.*//' /etc/debian_version | sed 's/\\..*//')\"\n case \"$dist_version\" in\n 14)\n dist_version=\"forky\"\n ;;\n 13)\n dist_version=\"trixie\"\n ;;\n 12)\n dist_version=\"bookworm\"\n ;;\n 11)\n dist_version=\"bullseye\"\n ;;\n 10)\n dist_version=\"buster\"\n ;;\n 9)\n dist_version=\"stretch\"\n ;;\n 8|'Kali Linux 2')\n dist_version=\"jessie\"\n ;;\n 7)\n dist_version=\"wheezy\"\n ;;\n esac\n elif [ -r /etc/redhat-release ] && [ -z \"$lsb_dist\" ]; then\n lsb_dist=redhat\n # Extract version from redhat-release file\n dist_version=\"$(sed 's/.*release \\([0-9.]*\\).*/\\1/' /etc/redhat-release)\"\n fi\n fi\n}\n\n# Set up sudo command if necessary\nsetup_sudo() {\n sh_c='sh -c'\n if [ \"$user\" != 'root' ]; then\n if command_exists sudo; then\n sh_c='sudo -E sh -c'\n elif command_exists su; then\n sh_c='su -c'\n else\n echo \"Error: this installer needs the ability to run commands as root.\"\n echo \"We are unable to find either 'sudo' or 'su' available to make this happen.\"\n exit 1\n fi\n fi\n echo \"# Using command executor: $sh_c\"\n}\n\n# Refine distribution version detection based on the distro\nrefine_distribution_version() {\n case \"$lsb_dist\" in\n ubuntu)\n if command_exists lsb_release; then\n dist_version=\"$(lsb_release --codename | cut -f2)\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/lsb-release ]; then\n \n dist_version=\"$(. /etc/lsb-release && echo \"$DISTRIB_CODENAME\")\"\n fi\n ;;\n\n debian|raspbian)\n # If we only have a number, map it to a codename for better recognition\n if echo \"$dist_version\" | grep -qE '^[0-9]+$'; then\n case \"$dist_version\" in\n 14)\n dist_version=\"forky\"\n ;;\n 13)\n dist_version=\"trixie\"\n ;;\n 12)\n dist_version=\"bookworm\"\n ;;\n 11)\n dist_version=\"bullseye\"\n ;;\n 10)\n # Handle special case for Buster\n dist_version=\"buster\"\n if [ \"$user\" = 'root' ]; then\n apt-get update --allow-releaseinfo-change || true\n elif command_exists sudo; then\n sudo apt-get update --allow-releaseinfo-change || true\n fi\n ;;\n 9)\n dist_version=\"stretch\"\n ;;\n 8)\n dist_version=\"jessie\"\n ;;\n 7)\n dist_version=\"wheezy\"\n ;;\n esac\n fi\n ;;\n\n centos|rhel|fedora|ol)\n # Make sure we have a version number\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/redhat-release ]; then\n dist_version=\"$(sed 's/.*release \\([0-9.]*\\).*/\\1/' /etc/redhat-release)\"\n fi\n ;;\n\n sles|opensuse)\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n # Fallback for older versions\n if [ -z \"$dist_version\" ] && [ -r /etc/SuSE-release ]; then\n dist_version=\"$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = //')\"\n fi\n # Ensure version is in the correct format (e.g., 15.4 for SLES 15 SP4)\n if [ -n \"$dist_version\" ]; then\n # Remove any non-numeric characters except dots\n dist_version=\"$(echo \"$dist_version\" | sed 's/[^0-9.]//g')\"\n fi\n # Normalize distribution name\n if [ \"$lsb_dist\" = \"sles\" ]; then\n lsb_dist=\"sles\"\n elif [ \"$lsb_dist\" = \"opensuse\" ]; then\n lsb_dist=\"opensuse\"\n fi\n ;;\n\n *)\n if command_exists lsb_release; then\n dist_version=\"$(lsb_release --release | cut -f2)\"\n fi\n if [ -z \"$dist_version\" ] && [ -r /etc/os-release ]; then\n \n dist_version=\"$(. /etc/os-release && echo \"$VERSION_ID\")\"\n fi\n ;;\n esac\n}\n\n# Detect init system \ndetect_init_system() {\n if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then\n INIT_SYSTEM=\"systemd\"\n elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then\n INIT_SYSTEM=\"upstart\"\n elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then\n INIT_SYSTEM=\"openrc\"\n elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then\n INIT_SYSTEM=\"s6\"\n elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then\n INIT_SYSTEM=\"runit\"\n elif [ -d /etc/init.d ]; then\n INIT_SYSTEM=\"sysvinit\"\n else\n INIT_SYSTEM=\"unknown\"\n fi\n export INIT_SYSTEM\n echo \"# Detected init system: $INIT_SYSTEM\"\n}\n\n# Detect package type (deb, rpm, or other)\ndetect_package_type() {\n case \"$lsb_dist\" in\n debian|ubuntu|raspbian|mendel)\n PACKAGE_TYPE=\"deb\"\n ;;\n fedora|centos|rhel|ol|sles|opensuse*)\n PACKAGE_TYPE=\"rpm\"\n ;;\n alpine)\n PACKAGE_TYPE=\"apk\"\n ;;\n *)\n if command_exists apt-get || command_exists dpkg; then\n PACKAGE_TYPE=\"deb\"\n elif command_exists yum || command_exists dnf || command_exists zypper; then\n PACKAGE_TYPE=\"rpm\"\n elif command_exists apk; then\n PACKAGE_TYPE=\"apk\"\n else\n PACKAGE_TYPE=\"other\"\n fi\n ;;\n esac\n export PACKAGE_TYPE\n echo \"# Detected package type: $PACKAGE_TYPE\"\n}\n\n# Init function\ninit() {\n # Detect basic distribution info\n get_distribution\n \n # Set up sudo for privileged commands\n setup_sudo\n \n # Refine version information\n refine_distribution_version\n \n # Check if this is a forked distro\n check_forked\n \n # Detect init system and package type (for universal OS/init support)\n detect_init_system\n detect_package_type\n \n # Print final distribution information\n echo \"----------------------------------------\"\n echo \"Linux Distribution: $lsb_dist\"\n echo \"Version: $dist_version\"\n echo \"Init system: $INIT_SYSTEM\"\n echo \"Package type: $PACKAGE_TYPE\"\n echo \"----------------------------------------\"\n \n}\n"), - } - filey := &embedded.EmbeddedFile{ - Filename: "container-controller/install_container_engine.sh", - FileModTime: time.Unix(1775031500, 0), - - Content: string("#!/bin/sh\n# Script to install Docker/Podman based on Linux distribution\n# Sources init.sh for distribution detection\n\nset -x\nset -e\n\nCONTAINER_ENGINE_MSG=\"This operating system does not support automatic container engine installation. Please install Docker 25+ or Podman 4+ on the target host and re-run, or use an airgap deployment with a pre-installed engine.\"\n\ncheck_docker_version() {\n docker_version_num=0\n if command -v docker >/dev/null 2>&1; then\n raw=$(docker -v 2>/dev/null | sed 's/.*version \\([^,]*\\),.*/\\1/' | tr -d '.')\n [ -n \"$raw\" ] && docker_version_num=\"$raw\"\n fi\n [ \"$docker_version_num\" -ge 2500 ] 2>/dev/null || return 1\n}\n\ncheck_podman_version() {\n podman_version_num=0\n if command -v podman >/dev/null 2>&1; then\n raw=$(podman --version 2>/dev/null | sed -n 's/.*version \\([0-9][0-9]*\\).*/\\1/p')\n [ -n \"$raw\" ] && podman_version_num=\"$raw\"\n fi\n [ \"$podman_version_num\" -ge 4 ] 2>/dev/null || return 1\n}\n\nstart_docker() {\n set +e\n if $sh_c \"docker ps\" >/dev/null 2>&1; then\n set -e\n return 0\n fi\n err_code=1\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl start docker\" >/dev/null 2>&1\n err_code=$?\n ;;\n sysvinit)\n $sh_c \"service docker start\" >/dev/null 2>&1 || $sh_c \"/etc/init.d/docker start\" >/dev/null 2>&1\n err_code=$?\n ;;\n openrc)\n $sh_c \"rc-service docker start\" >/dev/null 2>&1\n err_code=$?\n ;;\n *)\n $sh_c \"/etc/init.d/docker start\" >/dev/null 2>&1\n err_code=$?\n [ $err_code -ne 0 ] && $sh_c \"systemctl start docker\" >/dev/null 2>&1 && err_code=0\n [ $err_code -ne 0 ] && $sh_c \"service docker start\" >/dev/null 2>&1 && err_code=0\n [ $err_code -ne 0 ] && $sh_c \"snap start docker\" >/dev/null 2>&1 && err_code=0\n ;;\n esac\n set -e\n if [ $err_code -ne 0 ]; then\n echo \"Could not start Docker daemon\"\n exit 1\n fi\n}\n\nstart_podman() {\n set +e\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl start podman\" >/dev/null 2>&1\n $sh_c \"systemctl start podman.socket\" >/dev/null 2>&1\n ;;\n sysvinit)\n $sh_c \"service podman start\" >/dev/null 2>&1 || $sh_c \"/etc/init.d/podman start\" >/dev/null 2>&1\n ;;\n openrc)\n $sh_c \"rc-service podman start\" >/dev/null 2>&1\n ;;\n *)\n $sh_c \"systemctl start podman\" >/dev/null 2>&1 || true\n $sh_c \"systemctl start podman.socket\" >/dev/null 2>&1 || true\n $sh_c \"service podman start\" >/dev/null 2>&1 || true\n ;;\n esac\n set -e\n}\n\n\ndo_modify_daemon() {\n # Skip for Podman installations\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"# Configuring Podman for CDI directory support...\"\n\n # Create CDI directories\n $sh_c \"mkdir -p /etc/cdi /var/run/cdi\"\n\n # Ensure /etc/containers exists\n $sh_c \"mkdir -p /etc/containers\"\n\n # Create containers.conf if it doesn't exist\n if [ ! -f \"/etc/containers/containers.conf\" ]; then\n $sh_c 'cat > /etc/containers/containers.conf <> /etc/containers/containers.conf'\n fi\n fi\n\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl enable podman\" 2>/dev/null || true\n $sh_c \"systemctl enable podman.socket\" 2>/dev/null || true\n ;;\n openrc)\n $sh_c \"rc-update add podman default\" 2>/dev/null || true\n ;;\n sysvinit)\n $sh_c \"update-rc.d podman defaults\" 2>/dev/null || $sh_c \"chkconfig podman on\" 2>/dev/null || true\n ;;\n *) ;;\n esac\n start_podman\n return\n fi\n\n # Original Docker daemon configuration\n if [ ! -f /etc/docker/daemon.json ]; then\n echo \"Creating /etc/docker/daemon.json...\"\n $sh_c \"mkdir -p /etc/docker\"\n $sh_c 'cat > /etc/docker/daemon.json << EOF\n{\n\t\"storage-driver\": \"overlayfs\",\n \"features\": {\n \"containerd-snapshotter\": true,\n \"cdi\": true\n },\n \"cdi-spec-dirs\": [\"/etc/cdi/\", \"/var/run/cdi\"]\n}\nEOF'\n else\n echo \"/etc/docker/daemon.json already exists\"\n fi\n echo \"Restarting Docker daemon...\"\n case \"${INIT_SYSTEM:-unknown}\" in\n systemd)\n $sh_c \"systemctl daemon-reload\"\n $sh_c \"systemctl restart docker\"\n ;;\n *)\n $sh_c \"systemctl daemon-reload\" 2>/dev/null || true\n $sh_c \"systemctl restart docker\" 2>/dev/null || start_docker\n ;;\n esac\n}\n\ndo_install_container_engine() {\n if [ \"$PACKAGE_TYPE\" = \"apk\" ]; then\n if command_exists docker && check_docker_version; then\n echo \"# Docker already installed (>= 25)\"\n start_docker\n do_modify_daemon\n return 0\n fi\n echo \"# Installing Docker on Alpine...\"\n $sh_c \"apk add docker\"\n $sh_c \"rc-update add docker default\"\n $sh_c \"service docker start\"\n $sh_c \"addgroup $user docker\"\n if ! command_exists docker; then\n echo \"Failed to install Docker\"\n exit 1\n fi\n if ! check_docker_version; then\n echo \"Error: Docker 25+ is required. Please upgrade the Docker package or install Docker 25+ manually.\"\n exit 1\n fi\n start_docker\n do_modify_daemon\n return 0\n fi\n\n if [ \"$PACKAGE_TYPE\" = \"other\" ]; then\n if check_docker_version; then\n USE_PODMAN=\"false\"\n echo \"# Docker (>= 25) found; using Docker.\"\n start_docker\n do_modify_daemon\n return 0\n fi\n if check_podman_version; then\n USE_PODMAN=\"true\"\n echo \"# Podman (>= 4) found; using Podman.\"\n do_modify_daemon\n return 0\n fi\n echo \"Error: $CONTAINER_ENGINE_MSG\"\n exit 1\n fi\n\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"# Installing Podman and related packages...\"\n case \"$lsb_dist\" in\n fedora|centos|rhel|ol)\n $sh_c \"yum install -y podman crun podman-docker\"\n ;;\n sles|opensuse*)\n $sh_c \"zypper install -y podman crun podman-docker\"\n ;;\n esac\n if ! check_podman_version; then\n echo \"Error: Podman 4+ is required. Please upgrade Podman.\"\n exit 1\n fi\n do_modify_daemon\n return\n fi\n\n if command_exists docker; then\n docker_version=$(docker -v 2>/dev/null | sed 's/.*version \\([^,]*\\),.*/\\1/' | tr -d '.')\n if [ -n \"$docker_version\" ] && [ \"$docker_version\" -ge 2500 ] 2>/dev/null; then\n echo \"# Docker already installed (>= 25)\"\n start_docker\n do_modify_daemon\n return\n fi\n fi\n\n echo \"# Installing Docker...\"\n case \"$lsb_dist\" in\n debian|ubuntu|raspbian)\n case \"$dist_version\" in\n \"stretch\")\n $sh_c \"apt install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common\"\n curl -fsSL https://download.docker.com/linux/debian/gpg | $sh_c \"apt-key add -\"\n $sh_c \"add-apt-repository \\\"deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/debian $(lsb_release -cs) stable\\\"\"\n $sh_c \"apt update -y\"\n $sh_c \"apt install -y docker-ce\"\n ;;\n *)\n curl -fsSL https://get.docker.com/ | $sh_c \"sh\"\n ;;\n esac\n ;;\n *)\n curl -fsSL https://get.docker.com/ | $sh_c \"sh\"\n ;;\n esac\n\n if ! command_exists docker; then\n echo \"Failed to install Docker\"\n exit 1\n fi\n if ! check_docker_version; then\n echo \"Error: Docker 25+ is required. Please upgrade Docker.\"\n exit 1\n fi\n start_docker\n do_modify_daemon\n}\n\n# Check if we should use Podman based on distribution\ndetermine_container_engine() {\n USE_PODMAN=\"false\"\n case \"$lsb_dist\" in\n fedora|centos|rhel|ol|sles|opensuse*)\n USE_PODMAN=\"true\"\n echo \"# Using Podman for $lsb_dist\"\n ;;\n *)\n echo \"# Using Docker for $lsb_dist\"\n ;;\n esac\n}\n\n# Source init.sh to get distribution info\n. /etc/iofog/controller/init.sh\ninit\n\n# Configure container engine based on distribution\ndetermine_container_engine\n\n# Install appropriate container engine\ndo_install_container_engine\n\necho \"# Installation completed successfully\""), - } - filez := &embedded.EmbeddedFile{ - Filename: "container-controller/install_iofog.sh", - FileModTime: time.Unix(1775031637, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\n# INSTALL_DIR=\"/opt/iofog\"\nTMP_DIR=\"/tmp/iofog\"\nETC_DIR=\"/etc/iofog/controller\"\nCONTROLLER_LOG_FOLDER=/var/log/iofog-controller\nCONTROLLER_CONTAINER_NAME=\"iofog-controller\"\n\ncommand_exists() {\n command -v \"$1\" >/dev/null 2>&1\n}\n\ndo_stop_iofog_controller() {\n if ! command_exists iofog-controller; then\n return 0\n fi\n case \"${INIT_SYSTEM:-systemd}\" in\n systemd)\n sudo systemctl stop iofog-controller 2>/dev/null || true\n ;;\n sysvinit|openrc)\n sudo service iofog-controller stop 2>/dev/null || sudo /etc/init.d/iofog-controller stop 2>/dev/null || true\n ;;\n s6)\n sudo s6-svc -d /etc/s6/sv/iofog-controller 2>/dev/null || true\n ;;\n runit)\n sudo sv stop iofog-controller 2>/dev/null || true\n ;;\n upstart)\n sudo initctl stop iofog-controller 2>/dev/null || true\n ;;\n *)\n sudo systemctl stop iofog-controller 2>/dev/null || sudo service iofog-controller stop 2>/dev/null || true\n ;;\n esac\n (docker stop ${CONTROLLER_CONTAINER_NAME} 2>/dev/null || podman stop ${CONTROLLER_CONTAINER_NAME} 2>/dev/null) || true\n}\n\ndo_install_iofog_controller() {\n echo \"# Installing ioFog controller...\"\n\n for FOLDER in ${ETC_DIR} ${CONTROLLER_LOG_FOLDER}; do\n if [ ! -d \"$FOLDER\" ]; then\n echo \"Creating folder: $FOLDER\"\n sudo mkdir -p \"$FOLDER\"\n sudo chmod 775 \"$FOLDER\"\n fi\n done\n\n USE_PODMAN=\"false\"\n case \"$lsb_dist\" in\n rhel|centos|fedora|ol|sles|opensuse*) USE_PODMAN=\"true\" ;;\n esac\n\n CONTROLLER_RUN_ARGS=\"-e IOFOG_CONTROLLER_IMAGE=${controller_image} --env-file ${ETC_DIR}/iofog-controller.env -v iofog-controller-db:/home/runner/.npm-global/lib/node_modules/@eclipse-iofog/iofogcontroller/src/data/sqlite_files/:rw -v iofog-controller-log:/var/log/iofog-controller:rw -p 51121:51121 -p 80:8008 --stop-timeout 60 ${controller_image}\"\n\n if [ \"${INIT_SYSTEM:-systemd}\" = \"systemd\" ]; then\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n echo \"Creating Quadlet container file for ioFog controller...\"\n sudo mkdir -p /etc/containers/systemd\n cat < /dev/null\n[Unit]\nDescription=ioFog Controller Service\nAfter=podman.service\nRequires=podman.service\n\n[Container]\nContainerName=${CONTROLLER_CONTAINER_NAME}\nImage=${controller_image}\nPodmanArgs=--stop-timeout=60\nEnvironment=IOFOG_CONTROLLER_IMAGE=${controller_image}\nEnvironmentFile=${ETC_DIR}/iofog-controller.env\nVolume=iofog-controller-db:/home/runner/.npm-global/lib/node_modules/@eclipse-iofog/iofogcontroller/src/data/sqlite_files/:rw\nVolume=iofog-controller-log:/var/log/iofog-controller:rw\nPublishPort=51121:51121\nPublishPort=80:8008\nLogDriver=journald\n\n[Service]\nRestart=always\n\n[Install]\nWantedBy=default.target\nEOF\n sudo systemctl daemon-reload\n sudo systemctl restart podman 2>/dev/null || true\n sudo systemctl enable iofog-controller.service\n sudo systemctl start iofog-controller.service\n else\n echo \"Creating systemd service for ioFog controller...\"\n cat < /dev/null\n[Unit]\nDescription=ioFog Controller Service\nAfter=docker.service\nRequires=docker.service\n\n[Service]\nTimeoutStartSec=0\nRestart=always\nExecStartPre=-/usr/bin/docker rm -f ${CONTROLLER_CONTAINER_NAME}\nExecStart=/usr/bin/docker run --rm --name ${CONTROLLER_CONTAINER_NAME} \\\\\n${CONTROLLER_RUN_ARGS}\nExecStop=/usr/bin/docker stop ${CONTROLLER_CONTAINER_NAME}\n\n[Install]\nWantedBy=default.target\nEOF\n sudo systemctl daemon-reload\n sudo systemctl enable iofog-controller.service\n sudo systemctl start iofog-controller.service\n fi\n else\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n RUN_CMD=\"podman run --rm -d --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}\"\n RUN_CMD_FG=\"podman run --rm --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}\"\n else\n RUN_CMD=\"docker run --rm -d --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}\"\n RUN_CMD_FG=\"docker run --rm --name ${CONTROLLER_CONTAINER_NAME} ${CONTROLLER_RUN_ARGS}\"\n fi\n if [ \"$USE_PODMAN\" = \"true\" ]; then\n STOP_CMD=\"podman stop ${CONTROLLER_CONTAINER_NAME}\"\n else\n STOP_CMD=\"docker stop ${CONTROLLER_CONTAINER_NAME}\"\n fi\n\n case \"$INIT_SYSTEM\" in\n sysvinit|openrc)\n sudo tee /etc/init.d/iofog-controller > /dev/null </dev/null | grep -q \"^${CONTROLLER_CONTAINER_NAME}\\$\"; then exit 0; fi\n if podman ps --format '{{.Names}}' 2>/dev/null | grep -q \"^${CONTROLLER_CONTAINER_NAME}\\$\"; then exit 0; fi\n $RUN_CMD\n ;;\n stop) $STOP_CMD 2>/dev/null || true ;;\n restart) \\$0 stop; \\$0 start ;;\n status)\n if docker ps --format '{{.Names}}' 2>/dev/null | grep -q \"^${CONTROLLER_CONTAINER_NAME}\\$\"; then echo \"running\"; exit 0; fi\n if podman ps --format '{{.Names}}' 2>/dev/null | grep -q \"^${CONTROLLER_CONTAINER_NAME}\\$\"; then echo \"running\"; exit 0; fi\n echo \"stopped\"; exit 1\n ;;\n *) echo \"Usage: \\$0 {start|stop|restart|status}\"; exit 1 ;;\nesac\nexit 0\nINITSCRIPT\n sudo chmod +x /etc/init.d/iofog-controller\n if [ \"$INIT_SYSTEM\" = \"openrc\" ]; then\n sudo rc-update add iofog-controller default 2>/dev/null || true\n sudo rc-service iofog-controller start\n else\n sudo update-rc.d iofog-controller defaults 2>/dev/null || sudo chkconfig iofog-controller on 2>/dev/null || true\n sudo service iofog-controller start 2>/dev/null || sudo /etc/init.d/iofog-controller start\n fi\n ;;\n s6)\n sudo mkdir -p /etc/s6/sv/iofog-controller\n printf '#!/bin/sh\\nexec %s\\n' \"$RUN_CMD_FG\" | sudo tee /etc/s6/sv/iofog-controller/run > /dev/null\n sudo chmod +x /etc/s6/sv/iofog-controller/run\n [ -d /etc/s6/adminsv/default ] && sudo ln -sf /etc/s6/sv/iofog-controller /etc/s6/adminsv/default/iofog-controller 2>/dev/null || true\n sudo s6-svc -u /etc/s6/sv/iofog-controller 2>/dev/null || true\n ;;\n runit)\n sudo mkdir -p /etc/runit/sv/iofog-controller\n printf '#!/bin/sh\\nexec %s\\n' \"$RUN_CMD_FG\" | sudo tee /etc/runit/sv/iofog-controller/run > /dev/null\n sudo chmod +x /etc/runit/sv/iofog-controller/run\n [ -d /var/service ] && sudo ln -sf /etc/runit/sv/iofog-controller /var/service/iofog-controller 2>/dev/null || true\n [ -d /etc/runit/runsvdir/default ] && sudo ln -sf /etc/runit/sv/iofog-controller /etc/runit/runsvdir/default/iofog-controller 2>/dev/null || true\n sudo sv start iofog-controller 2>/dev/null || true\n ;;\n upstart)\n printf 'description \"IoFog Controller container\"\\nstart on runlevel [2345]\\nstop on runlevel [!2345]\\nrespawn\\nrespawn limit 10 5\\nexec %s\\n' \"$RUN_CMD_FG\" | sudo tee /etc/init/iofog-controller.conf > /dev/null\n sudo initctl reload-configuration 2>/dev/null || true\n sudo initctl start iofog-controller 2>/dev/null || true\n ;;\n *)\n sudo tee /etc/init.d/iofog-controller > /dev/null < /dev/null\n#!/bin/sh\nCONTAINER_NAME=\"iofog-controller\"\nif ! podman ps --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Error: The iofog-controller container is not running.\"\n exit 1\nfi\nexec podman exec ${CONTAINER_NAME} iofog-controller \"$@\"\nEOF\n else\n cat <<'EOF' | sudo tee ${EXECUTABLE_FILE} > /dev/null\n#!/bin/sh\nCONTAINER_NAME=\"iofog-controller\"\nif ! docker ps --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Error: The iofog-controller container is not running.\"\n exit 1\nfi\nexec docker exec ${CONTAINER_NAME} iofog-controller \"$@\"\nEOF\n fi\n sudo chmod +x ${EXECUTABLE_FILE}\n\n echo \"ioFog controller installation completed!\"\n}\n\n# main\ncontroller_image=\"$1\"\n\n. /etc/iofog/controller/init.sh\ninit\ndo_stop_iofog_controller\ndo_install_iofog_controller\n"), - } - file10 := &embedded.EmbeddedFile{ - Filename: "container-controller/set_env.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nETC_DIR=\"/etc/iofog/controller\"\nENV_FILE_NAME=iofog-controller.env # Used as an env file in systemd\n\nENV_FILE=\"$ETC_DIR/$ENV_FILE_NAME\"\n\n# Create folder\nmkdir -p \"$ETC_DIR\"\n\n# Env file (for systemd)\nrm -f \"$ENV_FILE\"\ntouch \"$ENV_FILE\"\n\nfor var in \"$@\"\ndo\n echo \"$var\" >> \"$ENV_FILE\"\ndone"), - } - file11 := &embedded.EmbeddedFile{ - Filename: "container-controller/uninstall_iofog.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\n\nCONTROLLER_LOG_DIR=\"iofog-controller-log\"\nCONTAINER_NAME=\"iofog-controller\"\nEXECUTABLE_FILE=/usr/local/bin/iofog-controller\nCONTROLLER_DB=iofog-controller-db\n\n\ndo_uninstall_controller() {\n echo \"# Removing ioFog controller...\"\n\n case \"$lsb_dist\" in\n rhel|fedora|centos|ol|sles|opensuse*) CONTAINER_RUNTIME=\"podman\" ;;\n *) CONTAINER_RUNTIME=\"docker\" ;;\n esac\n\n case \"${INIT_SYSTEM:-systemd}\" in\n systemd)\n for f in /etc/systemd/system/iofog-controller.service /etc/containers/systemd/iofog-controller.container; do\n if [ -f \"$f\" ]; then\n echo \"Disabling and stopping systemd service...\"\n sudo systemctl stop iofog-controller.service 2>/dev/null || true\n sudo systemctl disable iofog-controller.service 2>/dev/null || true\n sudo rm -f \"$f\"\n sudo systemctl daemon-reload\n break\n fi\n done\n ;;\n sysvinit|openrc)\n if [ -f /etc/init.d/iofog-controller ]; then\n sudo service iofog-controller stop 2>/dev/null || sudo /etc/init.d/iofog-controller stop 2>/dev/null || true\n [ \"$INIT_SYSTEM\" = \"openrc\" ] && sudo rc-update del iofog-controller default 2>/dev/null || true\n sudo update-rc.d -f iofog-controller remove 2>/dev/null || sudo chkconfig --del iofog-controller 2>/dev/null || true\n sudo rm -f /etc/init.d/iofog-controller\n fi\n ;;\n s6)\n sudo s6-svc -d /etc/s6/sv/iofog-controller 2>/dev/null || true\n sudo rm -rf /etc/s6/sv/iofog-controller\n [ -L /etc/s6/adminsv/default/iofog-controller ] && sudo rm -f /etc/s6/adminsv/default/iofog-controller\n ;;\n runit)\n sudo sv stop iofog-controller 2>/dev/null || true\n [ -L /var/service/iofog-controller ] && sudo rm -f /var/service/iofog-controller\n [ -L /etc/runit/runsvdir/default/iofog-controller ] && sudo rm -f /etc/runit/runsvdir/default/iofog-controller\n sudo rm -rf /etc/runit/sv/iofog-controller\n ;;\n upstart)\n sudo initctl stop iofog-controller 2>/dev/null || true\n sudo rm -f /etc/init/iofog-controller.conf\n ;;\n *)\n sudo systemctl stop iofog-controller 2>/dev/null || true\n sudo systemctl disable iofog-controller 2>/dev/null || true\n sudo rm -f /etc/systemd/system/iofog-controller.service /etc/containers/systemd/iofog-controller.container\n sudo systemctl daemon-reload 2>/dev/null || true\n [ -f /etc/init.d/iofog-controller ] && sudo /etc/init.d/iofog-controller stop 2>/dev/null || true\n sudo rm -f /etc/init.d/iofog-controller\n ;;\n esac\n\n if sudo ${CONTAINER_RUNTIME} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${CONTAINER_NAME}$\"; then\n echo \"Stopping and removing the ioFog controller container...\"\n sudo ${CONTAINER_RUNTIME} stop ${CONTAINER_NAME} 2>/dev/null || true\n sudo ${CONTAINER_RUNTIME} rm ${CONTAINER_NAME} 2>/dev/null || true\n fi\n\n # Remove config files\n echo \"Checking if the ${CONTAINER_RUNTIME} volume exists...\"\n\n if sudo ${CONTAINER_RUNTIME} volume inspect \"${CONTROLLER_DB}\" >/dev/null 2>&1; then\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' found. Removing...\"\n sudo ${CONTAINER_RUNTIME} volume rm \"${CONTROLLER_DB}\"\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' has been removed.\"\n else\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_DB}' does not exist. Skipping removal.\"\n fi\n\n # Remove log files\n echo \"Removing log files...\"\n if sudo ${CONTAINER_RUNTIME} volume inspect \"${CONTROLLER_LOG_DIR}\" >/dev/null 2>&1; then\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' found. Removing...\"\n sudo ${CONTAINER_RUNTIME} volume rm \"${CONTROLLER_LOG_DIR}\"\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' has been removed.\"\n else\n echo \"${CONTAINER_RUNTIME} volume '${CONTROLLER_LOG_DIR}' does not exist. Skipping removal.\"\n fi\n\n\n # Remove the executable script\n if [ -f ${EXECUTABLE_FILE} ]; then\n echo \"Removing the iofog-controller executable script...\"\n sudo rm -f ${EXECUTABLE_FILE}\n fi\n\n echo \"ioFog controller uninstalled successfully!\"\n}\n\n. /etc/iofog/controller/init.sh\ninit\n\ndo_uninstall_controller"), - } - file13 := &embedded.EmbeddedFile{ - Filename: "controller/check_prereqs.sh", - FileModTime: time.Unix(1775031365, 0), - - Content: string("#!/bin/sh\nset -x\n\n# Check can sudo without password\nif ! $(sudo ls /tmp/ > /dev/null); then\n\tMSG=\"Unable to successfully use sudo with user $USER on this host.\\nUser $USER must be in sudoers group and using sudo without password must be enabled.\\nPlease see iofog.org documentation for more details.\"\n\techo $MSG\n\texit 1\nfi"), - } - file14 := &embedded.EmbeddedFile{ - Filename: "controller/install_iofog.sh", - FileModTime: time.Unix(1775031678, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nINSTALL_DIR=\"/opt/iofog\"\nTMP_DIR=\"/tmp/iofog\"\nETC_DIR=\"/etc/iofog/controller\"\n\ncontroller_service() {\n USE_SYSTEMD=`grep -m1 -c systemd /proc/1/comm`\n USE_INITCTL=`which initctl | wc -l`\n USE_SERVICE=`which service | wc -l`\n\n if [ $USE_SYSTEMD -eq 1 ]; then\n cp \"$ETC_DIR/service/iofog-controller.systemd\" /etc/systemd/system/iofog-controller.service\n chmod 644 /etc/systemd/system/iofog-controller.service\n systemctl daemon-reload\n systemctl enable iofog-controller.service\n elif [ $USE_INITCTL -eq 1 ]; then\n cp \"$ETC_DIR/service/iofog-controller.initctl\" /etc/init/iofog-controller.conf\n initctl reload-configuration\n elif [ $USE_SERVICE -eq 1 ]; then\n cp \"$ETC_DIR/service/iofog-controller.update-rc\" /etc/init.d/iofog-controller\n chmod +x /etc/init.d/iofog-controller\n update-rc.d iofog-controller defaults\n else\n echo \"Unable to setup Controller startup script.\"\n fi\n}\n\ninstall_package() {\n\t\tif [ -z \"$(command -v apt)\" ]; then\n\t\t\techo \"Unsupported distro\"\n\t\t\texit 1\n\t\tfi\n\t\tapt update -qq\n\t\tapt install -y $1\n}\n\ninstall_deps() {\n\tif [ -z \"$(command -v curl)\" ]; then\n install_package \"curl\"\n\tfi\n\n\tif [ -z \"$(command -v lsof)\" ]; then\n install_package \"lsof\"\n\tfi\n\n\tif [ -z \"$(command -v make)\" ]; then\n install_package \"build-essential\"\n\tfi\n\n\tif [ -z \"$(command -v python2)\" ]; then\n install_package \"python2\"\n\tfi\n\n\tif [ -z \"$(command -v python3)\" ]; then\n install_package \"python3\"\n\tfi\n\n\tif [ -z \"$(command -v python-is-python3)\" ]; then\n install_package \"python-is-python3\"\n\tfi\n}\n\ncreate_logrotate() {\n cat < /etc/logrotate.d/iofog-controller\n/var/log/iofog-controller/iofog-controller.log {\n rotate 10\n size 100m\n compress\n notifempty\n missingok\n postrotate\n kill -HUP `cat $INSTALL_DIR/controller/lib/node_modules/@eclipse-iofog/iofogcontroller/src/iofog-controller.pid`\n}\nEOF\n chmod 644 /etc/logrotate.d/iofog-controller\n}\n\ndeploy_controller() {\n\t# Nuke any existing instances\n\tif [ ! -z \"$(lsof -ti tcp:51121)\" ]; then\n\t\tlsof -ti tcp:51121 | xargs kill\n\tfi\n\n# #\t If token is provided, set up private repo\n# \tif [ ! -z $token ]; then\n# \t\tif [ ! -z $(npmrc | grep iofog) ]; then\n# \t\t\tnpmrc -c iofog\n# \t\t\tnpmrc iofog\n# \t\tfi\n# \t\tcurl -s https://\"$token\":@packagecloud.io/install/repositories/\"$repo\"/script.node.sh?package_id=7463817 | force_npm=1 bash\n# \t\tmv ~/.npmrc ~/.npmrcs/npmrc\n# \t\tln -s ~/.npmrcs/npmrc ~/.npmrc\n# \telse\n# \t\tnpmrc default\n# \tfi\n\t# Save DB\n\tif [ -f \"$INSTALL_DIR/controller/lib/node_modules/@eclipse-iofog/iofogcontroller/package.json\" ]; then\n\t\t# If iofog-controller is not running, it will fail to stop - ignore that failure.\n\t\tnode $INSTALL_DIR/controller/lib/node_modules/@eclipse-iofog/iofogcontroller/scripts/scripts-api.js preuninstall > /dev/null 2>&1 || true\n\tfi\n\n\t# Install in temporary location\n\tmkdir -p \"$TMP_DIR/controller\"\n\tchmod 0777 \"$TMP_DIR/controller\"\n\tif [ -z $version ]; then\n\t\tnpm install -g -f @eclipse-iofog/iofogcontroller --unsafe-perm --prefix \"$TMP_DIR/controller\"\n\telse\n\t\tnpm install -g -f @eclipse-iofog/iofogcontroller --unsafe-perm --prefix \"$TMP_DIR/controller\"\n\tfi\n\t# Move files into $INSTALL_DIR/controller\n\tmkdir -p \"$INSTALL_DIR/\"\n\trm -rf \"$INSTALL_DIR/controller\" # Clean possible previous install\n\tmv \"$TMP_DIR/controller/\" \"$INSTALL_DIR/\"\n\n\t# Restore DB\n\tif [ -f \"$INSTALL_DIR/controller/lib/node_modules/@eclipse-iofog/iofogcontroller/package.json\" ]; then\n\t\tnode $INSTALL_DIR/controller/lib/node_modules/@eclipse-iofog/iofogcontroller/scripts/scripts-api.js postinstall > /dev/null 2>&1 || true\n\tfi\n\n\t# Symbolic links\n\tif [ ! -f \"/usr/local/bin/iofog-controller\" ]; then\n\t\tln -fFs \"$INSTALL_DIR/controller/bin/iofog-controller\" /usr/local/bin/iofog-controller\n\tfi\n\n\t# Set controller permissions\n\tchmod 744 -R \"$INSTALL_DIR/controller\"\n\n\t# Startup script\n\tcontroller_service\n\n\t# Run controller\n\t. /opt/iofog/config/controller/env.sh\n\tiofog-controller start\n}\n\n# main\nversion=\"$1\"\n# repo=$([ -z \"$2\" ] && echo \"iofog/iofog-controller-snapshots\" || echo \"$2\")\n# token=\"$3\"\n\ninstall_deps\ncreate_logrotate\ndeploy_controller\n"), - } - file15 := &embedded.EmbeddedFile{ - Filename: "controller/install_node.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nload_existing_nvm() {\n\tset +e\n\tif [ -z \"$(command -v nvm)\" ]; then\n\t\texport NVM_DIR=\"${HOME}/.nvm\"\n\t\tmkdir -p $NVM_DIR\n\t\tif [ -f \"$NVM_DIR/nvm.sh\" ]; then\n\t\t\t[ -s \"$NVM_DIR/nvm.sh\" ] && \\. \"$NVM_DIR/nvm.sh\" # This loads nvm\n\t\tfi\n\tfi\n\tset -e\n}\n\ninstall_node() {\n\tload_existing_nvm\n\tif [ -z \"$(command -v nvm)\" ]; then\n\t\tcurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/refs/tags/v0.40.1/install.sh | bash\n\t\texport NVM_DIR=\"${HOME}/.nvm\"\n\t\t[ -s \"$NVM_DIR/nvm.sh\" ] && \\. \"$NVM_DIR/nvm.sh\"\n\tfi\n\tnvm install v20.17.0\n\tnvm use v20.17.0\n\tln -Ffs $(which node) /usr/local/bin/node\n\tln -Ffs $(which npm) /usr/local/bin/npm\n\n\t# npmrc\n\tif [ -z \"$(command -v npmrc)\" ]; then\n\t\tnpm i npmrc -g\n\tfi\n\tln -Ffs $(which npmrc) /usr/local/bin/npmrc\n}\n\ninstall_node"), - } - file17 := &embedded.EmbeddedFile{ - Filename: "controller/service/iofog-controller.initctl", - FileModTime: time.Unix(1774969763, 0), - - Content: string("description \"ioFog Controller\"\n\nstart on (runlevel [2345])\nstop on (runlevel [!2345])\n\nrespawn\n\nscript\n . /opt/iofog/config/controller/env.sh\n exec /usr/local/bin/iofog-controller start\nend script"), - } - file18 := &embedded.EmbeddedFile{ - Filename: "controller/service/iofog-controller.systemd", - FileModTime: time.Unix(1774969763, 0), - - Content: string("[Unit]\nDescription=ioFog Controller\n\n[Service]\nType=forking\nExecStart=/usr/local/bin/iofog-controller start\nExecStop=/usr/local/bin/iofog-controller stop\nEnvironmentFile=/opt/iofog/config/controller/env.env\n\n[Install]\nWantedBy=multi-user.target\n"), - } - file19 := &embedded.EmbeddedFile{ - Filename: "controller/service/iofog-controller.update-rc", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\n\ncase \"$1\" in\n start)\n . /opt/iofog/controller/env.env\n /usr/local/bin/iofog-controller start\n ;;\n stop)\n /usr/local/bin/iofog-controller stop\n ;;\n restart)\n /usr/local/bin/iofog-controller stop\n . /opt/iofog/config/controller/env.sh\n /usr/local/bin/iofog-controller start\n ;;\n *)\n echo \"Usage: $0 {start|stop|restart}\"\nesac\n"), - } - file1a := &embedded.EmbeddedFile{ - Filename: "controller/set_env.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nCONF_FOLDER=/opt/iofog/config/controller\nSOURCE_FILE_NAME=env.sh # Used to source env variables\nENV_FILE_NAME=env.env # Used as an env file in systemd\n\nSOURCE_FILE=\"$CONF_FOLDER/$SOURCE_FILE_NAME\"\nENV_FILE=\"$CONF_FOLDER/$ENV_FILE_NAME\"\n\n# Create folder\nmkdir -p \"$CONF_FOLDER\"\n\n# Source file\necho \"#!/bin/sh\" > \"$SOURCE_FILE\"\n\n# Env file (for systemd)\nrm -f \"$ENV_FILE\"\ntouch \"$ENV_FILE\"\n\nfor var in \"$@\"\ndo\n echo \"export $var\" >> \"$SOURCE_FILE\"\n echo \"$var\" >> \"$ENV_FILE\"\ndone"), - } - file1b := &embedded.EmbeddedFile{ - Filename: "controller/uninstall_iofog.sh", - FileModTime: time.Unix(1774969763, 0), - - Content: string("#!/bin/sh\nset -x\nset -e\n\nCONTROLLER_DIR=\"/opt/iofog/controller/\"\nCONTROLLER_LOG_DIR=\"/var/log/iofog/\"\n\ndo_uninstall_controller() {\n # Remove folders\n sudo rm -rf $CONTROLLER_DIR\n sudo rm -rf $CONTROLLER_LOG_DIR\n\n # Remove symbolic links\n rm -f /usr/local/bin/iofog-controller\n\n # Remove service files\n USE_SYSTEMD=`grep -m1 -c systemd /proc/1/comm`\n USE_INITCTL=`which initctl | wc -l`\n USE_SERVICE=`which service | wc -l`\n\n if [ $USE_SYSTEMD -eq 1 ]; then\n systemctl stop iofog-controller.service\n rm -f /etc/systemd/system/iofog-controller.service\n elif [ $USE_INITCTL -eq 1 ]; then\n rm -f /etc/init/iofog-controller.conf\n elif [ $USE_SERVICE -eq 1 ]; then\n rm -f /etc/init.d/iofog-controller\n else\n echo \"Unable to setup Controller startup script.\"\n fi\n}\n\ndo_uninstall_controller"), - } - - // define dirs - dir1 := &embedded.EmbeddedDir{ - Filename: "", - DirModTime: time.Unix(1774969763, 0), - ChildFiles: []*embedded.EmbeddedFile{}, - } - dir2 := &embedded.EmbeddedDir{ - Filename: "agent", - DirModTime: time.Unix(1774969763, 0), - ChildFiles: []*embedded.EmbeddedFile{ - file3, // "agent/check_prereqs.sh" - file4, // "agent/init.sh" - file5, // "agent/install_container_engine.sh" - file6, // "agent/install_deps.sh" - file7, // "agent/install_iofog.sh" - file8, // "agent/install_java.sh" - file9, // "agent/uninstall_iofog.sh" - - }, - } - dira := &embedded.EmbeddedDir{ - Filename: "airgap-agent", - DirModTime: time.Unix(1774969763, 0), - ChildFiles: []*embedded.EmbeddedFile{ - fileb, // "airgap-agent/check_prereqs.sh" - filec, // "airgap-agent/init.sh" - filed, // "airgap-agent/install_container_engine.sh" - filee, // "airgap-agent/install_deps.sh" - filef, // "airgap-agent/install_iofog.sh" - fileg, // "airgap-agent/uninstall_iofog.sh" - - }, - } - dirh := &embedded.EmbeddedDir{ - Filename: "airgap-controller", - DirModTime: time.Unix(1774969763, 0), - ChildFiles: []*embedded.EmbeddedFile{ - filei, // "airgap-controller/check_prereqs.sh" - filej, // "airgap-controller/init.sh" - filek, // "airgap-controller/install_container_engine.sh" - filel, // "airgap-controller/install_iofog.sh" - filem, // "airgap-controller/set_env.sh" - filen, // "airgap-controller/uninstall_iofog.sh" - - }, - } - diro := &embedded.EmbeddedDir{ - Filename: "container-agent", - DirModTime: time.Unix(1774969763, 0), - ChildFiles: []*embedded.EmbeddedFile{ - filep, // "container-agent/check_prereqs.sh" - fileq, // "container-agent/init.sh" - filer, // "container-agent/install_container_engine.sh" - files, // "container-agent/install_deps.sh" - filet, // "container-agent/install_iofog.sh" - fileu, // "container-agent/uninstall_iofog.sh" - - }, - } - dirv := &embedded.EmbeddedDir{ - Filename: "container-controller", - DirModTime: time.Unix(1774969763, 0), - ChildFiles: []*embedded.EmbeddedFile{ - filew, // "container-controller/check_prereqs.sh" - filex, // "container-controller/init.sh" - filey, // "container-controller/install_container_engine.sh" - filez, // "container-controller/install_iofog.sh" - file10, // "container-controller/set_env.sh" - file11, // "container-controller/uninstall_iofog.sh" - - }, - } - dir12 := &embedded.EmbeddedDir{ - Filename: "controller", - DirModTime: time.Unix(1774969763, 0), - ChildFiles: []*embedded.EmbeddedFile{ - file13, // "controller/check_prereqs.sh" - file14, // "controller/install_iofog.sh" - file15, // "controller/install_node.sh" - file1a, // "controller/set_env.sh" - file1b, // "controller/uninstall_iofog.sh" - - }, - } - dir16 := &embedded.EmbeddedDir{ - Filename: "controller/service", - DirModTime: time.Unix(1774969763, 0), - ChildFiles: []*embedded.EmbeddedFile{ - file17, // "controller/service/iofog-controller.initctl" - file18, // "controller/service/iofog-controller.systemd" - file19, // "controller/service/iofog-controller.update-rc" - - }, - } - - // link ChildDirs - dir1.ChildDirs = []*embedded.EmbeddedDir{ - dir2, // "agent" - dira, // "airgap-agent" - dirh, // "airgap-controller" - diro, // "container-agent" - dirv, // "container-controller" - dir12, // "controller" - - } - dir2.ChildDirs = []*embedded.EmbeddedDir{} - dira.ChildDirs = []*embedded.EmbeddedDir{} - dirh.ChildDirs = []*embedded.EmbeddedDir{} - diro.ChildDirs = []*embedded.EmbeddedDir{} - dirv.ChildDirs = []*embedded.EmbeddedDir{} - dir12.ChildDirs = []*embedded.EmbeddedDir{ - dir16, // "controller/service" - - } - dir16.ChildDirs = []*embedded.EmbeddedDir{} - - // register embeddedBox - embedded.RegisterEmbeddedBox(`../../assets`, &embedded.EmbeddedBox{ - Name: `../../assets`, - Time: time.Unix(1774969763, 0), - Dirs: map[string]*embedded.EmbeddedDir{ - "": dir1, - "agent": dir2, - "airgap-agent": dira, - "airgap-controller": dirh, - "container-agent": diro, - "container-controller": dirv, - "controller": dir12, - "controller/service": dir16, - }, - Files: map[string]*embedded.EmbeddedFile{ - "agent/check_prereqs.sh": file3, - "agent/init.sh": file4, - "agent/install_container_engine.sh": file5, - "agent/install_deps.sh": file6, - "agent/install_iofog.sh": file7, - "agent/install_java.sh": file8, - "agent/uninstall_iofog.sh": file9, - "airgap-agent/check_prereqs.sh": fileb, - "airgap-agent/init.sh": filec, - "airgap-agent/install_container_engine.sh": filed, - "airgap-agent/install_deps.sh": filee, - "airgap-agent/install_iofog.sh": filef, - "airgap-agent/uninstall_iofog.sh": fileg, - "airgap-controller/check_prereqs.sh": filei, - "airgap-controller/init.sh": filej, - "airgap-controller/install_container_engine.sh": filek, - "airgap-controller/install_iofog.sh": filel, - "airgap-controller/set_env.sh": filem, - "airgap-controller/uninstall_iofog.sh": filen, - "container-agent/check_prereqs.sh": filep, - "container-agent/init.sh": fileq, - "container-agent/install_container_engine.sh": filer, - "container-agent/install_deps.sh": files, - "container-agent/install_iofog.sh": filet, - "container-agent/uninstall_iofog.sh": fileu, - "container-controller/check_prereqs.sh": filew, - "container-controller/init.sh": filex, - "container-controller/install_container_engine.sh": filey, - "container-controller/install_iofog.sh": filez, - "container-controller/set_env.sh": file10, - "container-controller/uninstall_iofog.sh": file11, - "controller/check_prereqs.sh": file13, - "controller/install_iofog.sh": file14, - "controller/install_node.sh": file15, - "controller/service/iofog-controller.initctl": file17, - "controller/service/iofog-controller.systemd": file18, - "controller/service/iofog-controller.update-rc": file19, - "controller/set_env.sh": file1a, - "controller/uninstall_iofog.sh": file1b, - }, - }) -} diff --git a/pkg/util/safepath.go b/pkg/util/safepath.go new file mode 100644 index 000000000..c1ba9ae93 --- /dev/null +++ b/pkg/util/safepath.go @@ -0,0 +1,132 @@ +package util + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +// ValidateReadableFile cleans a user-supplied path and ensures it is a readable regular file. +func ValidateReadableFile(path string) (string, error) { + clean := filepath.Clean(strings.TrimSpace(path)) + if clean == "" || clean == "." { + return "", fmt.Errorf("invalid file path") + } + info, err := os.Stat(clean) + if err != nil { + return "", err + } + if info.IsDir() { + return "", fmt.Errorf("%s is a directory", clean) + } + return clean, nil +} + +// ReadUserFile reads a validated user-supplied file path (CLI -f, --ca, etc.). +func ReadUserFile(path string) ([]byte, error) { + clean, err := ValidateReadableFile(path) + if err != nil { + return nil, err + } + return os.ReadFile(clean) // #nosec G304 -- path validated by ValidateReadableFile +} + +// CreateUserFile creates or truncates a user-supplied output path. +func CreateUserFile(path string, perm os.FileMode) (*os.File, error) { + clean, err := validateLocalPath(path) + if err != nil { + return nil, err + } + return os.OpenFile(clean, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm) // #nosec G304 -- validated path +} + +func validateLocalPath(path string) (string, error) { + clean := filepath.Clean(strings.TrimSpace(path)) + if clean == "" || clean == "." { + return "", fmt.Errorf("invalid file path") + } + for _, seg := range strings.Split(filepath.ToSlash(clean), "/") { + if seg == ".." { + return "", fmt.Errorf("path %q contains traversal", clean) + } + } + return clean, nil +} + +// ReadValidatedFile reads a path after rejecting traversal segments (cache/internal paths). +func ReadValidatedFile(path string) ([]byte, error) { + clean, err := validateLocalPath(path) + if err != nil { + return nil, err + } + return os.ReadFile(clean) // #nosec G304 -- validated path +} + +// OpenValidatedFile opens a path after rejecting traversal segments. +func OpenValidatedFile(path string) (*os.File, error) { + clean, err := validateLocalPath(path) + if err != nil { + return nil, err + } + return os.Open(clean) // #nosec G304 -- validated path +} + +// WriteValidatedFile writes data to a validated path. +func WriteValidatedFile(path string, data []byte, perm os.FileMode) error { + clean, err := validateLocalPath(path) + if err != nil { + return err + } + return os.WriteFile(clean, data, perm) +} + +// PathUnderRoot joins elems under root and rejects traversal outside root. +func PathUnderRoot(root string, elems ...string) (string, error) { + root = filepath.Clean(root) + all := append([]string{root}, elems...) + joined := filepath.Clean(filepath.Join(all...)) + if joined != root && !strings.HasPrefix(joined, root+string(os.PathSeparator)) { + return "", fmt.Errorf("path %q escapes root %q", joined, root) + } + return joined, nil +} + +// ReadFileUnderRoot reads rel from an opened root directory. +func ReadFileUnderRoot(root, rel string) ([]byte, error) { + f, err := OpenUnderRoot(root, rel) + if err != nil { + return nil, err + } + defer f.Close() + return io.ReadAll(f) +} + +// OpenUnderRoot opens rel inside root using os.OpenRoot when available. +func OpenUnderRoot(root, rel string) (*os.File, error) { + root = filepath.Clean(root) + rel = normalizeRelPath(rel) + if err := rejectTraversal(rel); err != nil { + return nil, err + } + r, err := os.OpenRoot(root) + if err != nil { + return nil, err + } + defer r.Close() + return r.Open(rel) +} + +func normalizeRelPath(rel string) string { + rel = filepath.ToSlash(filepath.Clean(rel)) + rel = strings.TrimPrefix(rel, "/") + return rel +} + +func rejectTraversal(rel string) error { + if rel == ".." || strings.HasPrefix(rel, "../") || strings.Contains(rel, "/../") { + return fmt.Errorf("path %q escapes root", rel) + } + return nil +} diff --git a/pkg/util/spinner.go b/pkg/util/spinner.go index cbce6d542..2bc9931f1 100644 --- a/pkg/util/spinner.go +++ b/pkg/util/spinner.go @@ -16,7 +16,7 @@ var ( ) func init() { - // Note: don't set the colour here, it will display the spinner when you don't want it to + // Note: don't set the color here, it will display the spinner when you don't want it to spin = spinner.New(spinner.CharSets[14], 100*time.Millisecond) } diff --git a/pkg/util/ssh.go b/pkg/util/ssh.go index f92b2aa0f..8a9c533dc 100644 --- a/pkg/util/ssh.go +++ b/pkg/util/ssh.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( @@ -64,7 +51,7 @@ func NewSecureShellClient(user, host, privKeyFilename string) (*SecureShellClien Auth: []ssh.AuthMethod{ key, }, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), + HostKeyCallback: cl.verifyHostKey(), } SSHVerbose("Config:") SSHVerbose(fmt.Sprintf("User: %s", cl.user)) @@ -156,7 +143,7 @@ func format(err error, stdout, stderr fmt.Stringer) error { func (cl *SecureShellClient) getPublicKey() (authMeth ssh.AuthMethod, err error) { // Read priv key file, MUST BE RSA SSHVerbose(fmt.Sprintf("Reading private key: %s", cl.privKeyFilename)) - key, err := os.ReadFile(cl.privKeyFilename) + key, err := ReadUserFile(cl.privKeyFilename) if err != nil { return } @@ -320,21 +307,25 @@ func (cl *SecureShellClient) CopyFolderTo(srcPath, destPath, permissions string, } } else { // Read the file - openFile, err := os.Open(filepath.Join(srcPath, file.Name())) + openFile, err := OpenUnderRoot(srcPath, file.Name()) if err != nil { return err } fileInfo, err := openFile.Stat() if err != nil { + IgnoreClose(openFile) return err } if fileInfo.Size() > maxFileSize { + IgnoreClose(openFile) return fmt.Errorf("file %s is too large (max size: %d bytes)", fileInfo.Name(), maxFileSize) } // Copy the file if err := cl.CopyTo(openFile, destPath, file.Name(), addLeadingZero(permissions), fileInfo.Size()); err != nil { + IgnoreClose(openFile) return err } + IgnoreClose(openFile) } } return nil diff --git a/pkg/util/ssh_hostkey.go b/pkg/util/ssh_hostkey.go new file mode 100644 index 000000000..8435de4b9 --- /dev/null +++ b/pkg/util/ssh_hostkey.go @@ -0,0 +1,111 @@ +package util + +import ( + "bufio" + "encoding/base64" + "fmt" + "net" + "os" + "path/filepath" + "strings" + + "github.com/mitchellh/go-homedir" + "golang.org/x/crypto/ssh" + "golang.org/x/term" +) + +var ( + sshHostKeyPromptFn = defaultSSHHostKeyPrompt + sshIsTerminalFn = term.IsTerminal + sshStdin = os.Stdin + sshStdout = os.Stdout +) + +func knownHostsRoot() (string, error) { + home, err := homedir.Dir() + if err != nil { + return "", err + } + return filepath.Join(home, ".iofog", "v3", "known_hosts"), nil +} + +func knownHostKeyFile(host string, port int) (string, error) { + root, err := knownHostsRoot() + if err != nil { + return "", err + } + safeHost := strings.NewReplacer(":", "_", "/", "_", "\\", "_").Replace(host) + name := fmt.Sprintf("%s_%d.pub", safeHost, port) + return filepath.Join(root, name), nil +} + +func loadKnownHostKey(path string) (ssh.PublicKey, error) { + root, err := knownHostsRoot() + if err != nil { + return nil, err + } + rel, err := filepath.Rel(root, path) + if err != nil { + return nil, err + } + data, err := ReadFileUnderRoot(root, rel) + if err != nil { + return nil, err + } + raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(data))) + if err != nil { + return nil, err + } + return ssh.ParsePublicKey(raw) +} + +func saveKnownHostKey(path string, key ssh.PublicKey) error { + if err := os.MkdirAll(filepath.Dir(path), DirPerm); err != nil { + return err + } + encoded := base64.StdEncoding.EncodeToString(key.Marshal()) + return os.WriteFile(path, []byte(encoded), FilePerm) +} + +func (cl *SecureShellClient) verifyHostKey() ssh.HostKeyCallback { + return func(_ string, _ net.Addr, key ssh.PublicKey) error { + path, err := knownHostKeyFile(cl.host, cl.port) + if err != nil { + return err + } + stored, err := loadKnownHostKey(path) + if err == nil { + if string(stored.Marshal()) != string(key.Marshal()) { + return fmt.Errorf("host key for %s:%d changed; remove %s to re-trust", cl.host, cl.port, path) + } + return nil + } + if !os.IsNotExist(err) { + return err + } + if !sshIsTerminalFn(int(sshStdin.Fd())) { + return fmt.Errorf("unknown SSH host key for %s:%d; run interactively to verify fingerprint", cl.host, cl.port) + } + SpinHandlePrompt() + defer SpinHandlePromptComplete() + + fingerprint := ssh.FingerprintSHA256(key) + if !sshHostKeyPromptFn(cl.host, cl.port, fingerprint) { + return fmt.Errorf("host key verification failed for %s:%d", cl.host, cl.port) + } + return saveKnownHostKey(path, key) + } +} + +func defaultSSHHostKeyPrompt(host string, port int, fingerprint string) bool { + fmt.Fprintf(sshStdout, "The authenticity of host '%s:%d' can't be established.\n", host, port) + fmt.Fprintf(sshStdout, "ED25519/EC/RSA key fingerprint is SHA256:%s.\n", fingerprint) + fmt.Fprint(sshStdout, "Are you sure you want to continue connecting (yes/no)? ") + reader := bufio.NewReader(sshStdin) + line, err := reader.ReadString('\n') + if err != nil { + return false + } + answer := strings.ToLower(strings.TrimSpace(line)) + return answer == "yes" || answer == "y" +} diff --git a/pkg/util/ssh_hostkey_test.go b/pkg/util/ssh_hostkey_test.go new file mode 100644 index 000000000..91e37dd4e --- /dev/null +++ b/pkg/util/ssh_hostkey_test.go @@ -0,0 +1,99 @@ +package util + +import ( + "crypto/ed25519" + "crypto/rand" + "strings" + "testing" + + "golang.org/x/crypto/ssh" +) + +func testSSHPublicKey(t *testing.T) ssh.PublicKey { + t.Helper() + _, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("generate ed25519 key: %v", err) + } + signer, err := ssh.NewSignerFromKey(priv) + if err != nil { + t.Fatalf("new signer: %v", err) + } + return signer.PublicKey() +} + +func TestKnownHostKeyFileIncludesPort(t *testing.T) { + path22, err := knownHostKeyFile("0.0.0.0", 22) + if err != nil { + t.Fatalf("knownHostKeyFile: %v", err) + } + if !strings.HasSuffix(path22, "0.0.0.0_22.pub") { + t.Fatalf("expected 0.0.0.0_22.pub suffix, got %q", path22) + } + + path61954, err := knownHostKeyFile("0.0.0.0", 61954) + if err != nil { + t.Fatalf("knownHostKeyFile: %v", err) + } + if !strings.HasSuffix(path61954, "0.0.0.0_61954.pub") { + t.Fatalf("expected 0.0.0.0_61954.pub suffix, got %q", path61954) + } + if path22 == path61954 { + t.Fatal("expected different known-host paths for different ports") + } +} + +func TestVerifyHostKeyUsesCurrentPort(t *testing.T) { + prevTerminal := sshIsTerminalFn + sshIsTerminalFn = func(int) bool { return false } + t.Cleanup(func() { sshIsTerminalFn = prevTerminal }) + + cl := &SecureShellClient{host: "0.0.0.0", port: 22} + cb := cl.verifyHostKey() + cl.SetPort(61954) + + err := cb("", nil, testSSHPublicKey(t)) + if err == nil { + t.Fatal("expected unknown host key error") + } + if !strings.Contains(err.Error(), "0.0.0.0:61954") { + t.Fatalf("expected error to reference dial port 61954, got: %v", err) + } + if strings.Contains(err.Error(), "0.0.0.0:22") { + t.Fatalf("expected error not to reference default port 22, got: %v", err) + } +} + +func TestVerifyHostKeyPausesSpinnerDuringPrompt(t *testing.T) { + SpinEnable(false) + t.Cleanup(func() { SpinEnable(true) }) + + SpinStart("deploying") + + prevTerminal := sshIsTerminalFn + sshIsTerminalFn = func(int) bool { return true } + t.Cleanup(func() { sshIsTerminalFn = prevTerminal }) + + promptCalled := false + prevPrompt := sshHostKeyPromptFn + sshHostKeyPromptFn = func(string, int, string) bool { + promptCalled = true + if isRunning { + t.Fatal("spinner should be paused during host key prompt") + } + return false + } + t.Cleanup(func() { sshHostKeyPromptFn = prevPrompt }) + + cl := &SecureShellClient{host: "example.com", port: 22} + err := cl.verifyHostKey()("", nil, testSSHPublicKey(t)) + if err == nil { + t.Fatal("expected verification failure") + } + if !promptCalled { + t.Fatal("expected host key prompt") + } + if !isRunning { + t.Fatal("spinner should resume after prompt") + } +} diff --git a/pkg/util/strings.go b/pkg/util/strings.go index 45677e76b..31ca61752 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( diff --git a/pkg/util/time.go b/pkg/util/time.go index 7629965d0..e0bcafe2f 100644 --- a/pkg/util/time.go +++ b/pkg/util/time.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( diff --git a/pkg/util/url.go b/pkg/util/url.go index c8cf9bdd0..7ae1e7f4d 100644 --- a/pkg/util/url.go +++ b/pkg/util/url.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( diff --git a/pkg/util/version.go b/pkg/util/version.go index 51db32f9a..4e3845c3b 100644 --- a/pkg/util/version.go +++ b/pkg/util/version.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import "fmt" @@ -22,21 +9,31 @@ var ( commit = "undefined" date = "undefined" - repo = "undefined" + cliBinaryName = "iofogctl" + cliCrdGroup = "iofog.org" + cliApiVersion = "iofog.org/v3" + cliCpCrName = "iofog" + imageRegistry = "ghcr.io/eclipse-iofog" + cliDocsUrl = "https://iofog.org" + packageRepoBase = "https://iofog.datasance.com" + ociSourceRepo = "https://github.com/eclipse-iofog/iofogctl" - controllerTag = "undefined" - agentTag = "undefined" - operatorTag = "undefined" - routerTag = "undefined" - natsTag = "undefined" - controllerVersion = "undefined" - agentVersion = "undefined" - debuggerTag = "undefined" + controllerTag = "undefined" + operatorTag = "undefined" + routerTag = "undefined" + natsTag = "undefined" + edgeletTag = "undefined" + controllerVersion = "undefined" + edgeletVersion = "undefined" + edgeletReleaseBase = "undefined" + edgeletBinaryVersion = "undefined" + edgeletGitHubRepo = "eclipse-iofog/edgelet" + debuggerTag = "undefined" ) const ( controllerImage = "controller" - agentImage = "agent" + edgeletImage = "edgelet" operatorImage = "operator" routerImage = "router" routerARMImage = "router" @@ -60,15 +57,40 @@ func GetVersion() Version { } } -func GetControllerVersion() string { return controllerVersion } -func GetAgentVersion() string { return agentVersion } +func GetCliBinaryName() string { return cliBinaryName } +func GetCliCrdGroup() string { return cliCrdGroup } +func GetCliApiVersion() string { return cliApiVersion } +func GetCliCpCrName() string { return cliCpCrName } +func GetImageRegistry() string { return imageRegistry } +func GetCliDocsUrl() string { return cliDocsUrl } +func GetPackageRepoBase() string { return packageRepoBase } +func GetOciSourceRepo() string { return ociSourceRepo } + +func GetControllerVersion() string { return controllerVersion } +func GetEdgeletVersion() string { return edgeletVersion } +func GetEdgeletReleaseBase() string { return edgeletReleaseBase } +func GetEdgeletBinaryVersion() string { return edgeletBinaryVersion } +func GetEdgeletGitHubRepo() string { return edgeletGitHubRepo } func GetControllerImage() string { - return fmt.Sprintf("%s/%s:%s", repo, controllerImage, controllerTag) + return fmt.Sprintf("%s/%s:%s", imageRegistry, controllerImage, controllerTag) +} +func GetEdgeletImage() string { + return fmt.Sprintf("%s/%s:%s", imageRegistry, edgeletImage, edgeletTag) +} +func GetOperatorImage() string { + return fmt.Sprintf("%s/%s:%s", imageRegistry, operatorImage, operatorTag) } -func GetAgentImage() string { return fmt.Sprintf("%s/%s:%s", repo, agentImage, agentTag) } -func GetOperatorImage() string { return fmt.Sprintf("%s/%s:%s", repo, operatorImage, operatorTag) } -func GetRouterImage() string { return fmt.Sprintf("%s/%s:%s", repo, routerImage, routerTag) } -func GetRouterARMImage() string { return fmt.Sprintf("%s/%s:%s", repo, routerARMImage, routerTag) } -func GetNatsImage() string { return fmt.Sprintf("%s/%s:%s", repo, natsImage, natsTag) } -func GetDebuggerImage() string { return fmt.Sprintf("%s/%s:%s", repo, debuggerImage, debuggerTag) } +func GetRouterImage() string { + return fmt.Sprintf("%s/%s:%s", imageRegistry, routerImage, routerTag) +} +func GetNatsImage() string { + return fmt.Sprintf("%s/%s:%s", imageRegistry, natsImage, natsTag) +} +func GetDebuggerImage() string { + return fmt.Sprintf("%s/%s:%s", imageRegistry, debuggerImage, debuggerTag) +} + +// Deprecated: Compatibility wrappers for Phase 1 compilation. Will be removed in Phase 4/5. +func GetAgentImage() string { return GetEdgeletImage() } +func GetAgentVersion() string { return GetEdgeletVersion() } diff --git a/pkg/util/yaml.go b/pkg/util/yaml.go index a8e9e31cf..e3c7274b7 100644 --- a/pkg/util/yaml.go +++ b/pkg/util/yaml.go @@ -1,16 +1,3 @@ -/* - * ******************************************************************************* - * * Copyright (c) 2023 Contributors to the Eclipse ioFog Project - * * - * * This program and the accompanying materials are made available under the - * * terms of the Eclipse Public License v. 2.0 which is available at - * * http://www.eclipse.org/legal/epl-2.0 - * * - * * SPDX-License-Identifier: EPL-2.0 - * ******************************************************************************* - * - */ - package util import ( @@ -21,7 +8,7 @@ import ( ) func UnmarshalYAML(filename string, object interface{}) error { - yamlFile, err := os.ReadFile(filename) + yamlFile, err := ReadUserFile(filename) if err != nil { return err } @@ -46,7 +33,7 @@ func printYAML(writer io.Writer, obj interface{}) error { } func FPrint(obj interface{}, filename string) error { - f, err := os.Create(filename) + f, err := CreateUserFile(filename, FilePerm) defer Log(f.Close) if err != nil { return err diff --git a/script/bootstrap.sh b/script/bootstrap.sh index 2416cd270..8322e511e 100755 --- a/script/bootstrap.sh +++ b/script/bootstrap.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# bootstrap.sh will check for and install any dependencies we have for building and using iofogctl +# bootstrap.sh will check for and install any dependencies we have for building the CLI # # Usage: ./bootstrap.sh # @@ -11,7 +11,7 @@ set -e # Import our helper functions . script/utils.sh -prettyTitle "Installing iofogctl Dependencies" +prettyTitle "Installing CLI Dependencies" echo # Check whether Brew is installed @@ -32,15 +32,6 @@ if ! checkForInstallation "go"; then exit 1 fi -# Is rice installed? -if [ -z $(command -v rice) ]; then - echo " Attempting to install 'rice'" - go install github.com/GeertJohan/go.rice/rice@latest - if [ -z $(command -v rice) ]; then - echo ' Could not find command rice after installation - is $GOBIN in $PATH?' - fi -fi - # Is bats installed? if ! checkForInstallation "bats"; then echoInfo " Attempting to install 'bats'" @@ -77,25 +68,6 @@ if ! checkForInstallation "golangci-lint"; then brew install golangci-lint brew upgrade golangci-lint else - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.33.0 - fi -fi - -# CI deps -if [ ! -z "$PIPELINE" ]; then - ## Is kubernetes-cli installed? - if ! checkForInstallation "kubectl"; then - OS=$(uname -s | tr A-Z a-z) - K8S_VERSION=1.22.7 - echoInfo " Attempting to install kubernetes-cli" - curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v"$K8S_VERSION"/bin/"$OS"/amd64/kubectl - chmod +x kubectl - sudo mv kubectl /usr/local/bin/ - fi - # Is go-junit-report installed? - if ! checkForInstallation "go-junit-report"; then - echoInfo " Attempting to install 'go-junit-report'" - go install github.com/jstemmer/go-junit-report@latest + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.12.2 fi - ## TODO: gcloud fi diff --git a/script/bump.sh b/script/bump.sh deleted file mode 100755 index 1533ebe84..000000000 --- a/script/bump.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/sh - -# Bump project version -if [ -z "$1" ]; then - echo "Provide a version argument e.g. 1.2.3-beta" -fi -if [ ! -f "version" ]; then - echo "File node found: $(pwd)/version" - exit 1 -fi - -# Extract version numbers and suffix -major=$1 -minor=$2 -patch=$3 -suffix=$4 -version=$1.$2.$3$4 - -# Update version file -sed -i.bkp "s/MAJOR=.*/MAJOR=$major/g" "version" -sed -i.bkp "s/MINOR=.*/MINOR=$minor/g" "version" -sed -i.bkp "s/PATCH=.*/PATCH=$patch/g" "version" -sed -i.bkp "s/SUFFIX=.*/SUFFIX=$suffix/g" "version" -rm "version.bkp" - -# Update Makefile -sed -i.bkp -E "s/(.*iofog-go-sdk\/v2@).*/\1v$version/g" Makefile -sed -i.bkp -E "s/(.*iofog-operator\/v2@).*/\1v$version/g" Makefile -sed -i.bkp -E "s/(.*-X.*Tag=).*/\1$version/g" Makefile -sed -i.bkp -E "s/(.*-X.*Version=).*/\1$version/g" Makefile -sed -i.bkp -E "s/(.*-X.*repo=).*/\1iofog/g" Makefile -rm Makefile.bkp - -# Update pipeline -for file in azure-pipelines.yaml test/env.sh; do - sed -i.bkp -E "s/(gcr\.io\/focal-freedom.*:).*/\1$version'/g" $file - sed -i.bkp -E "s/(_version: ).*/\1'$version'/g" $file - sed -i.bkp -E "s/(_VERSION=').*/\1$version'/g" $file - rm $file.bkp -done - -# Pull modules -make modules diff --git a/script/goreleaser-env-check.sh b/script/goreleaser-env-check.sh new file mode 100755 index 000000000..1221920f2 --- /dev/null +++ b/script/goreleaser-env-check.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ -z "${OPERATOR_VERSION:-}" ]; then + echo "Load release env first: eval \"\$(script/goreleaser-env.sh)\" (set GITHUB_REPOSITORY or FLAVOR to pick mirror)" >&2 + exit 1 +fi diff --git a/script/goreleaser-env.sh b/script/goreleaser-env.sh new file mode 100755 index 000000000..72c70864e --- /dev/null +++ b/script/goreleaser-env.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Export version pins (versions.mk) and flavor ldflags for goreleaser. +# Flavor resolution (first match wins): +# 1. FLAVOR env (manual override) +# 2. GITHUB_REPOSITORY (CI / local release smoke) +# 3. default iofog +# +# Usage: +# eval "$(script/goreleaser-env.sh)" +# GITHUB_REPOSITORY=Datasance/potctl script/goreleaser-release.sh release --clean +# FLAVOR=datasance script/goreleaser-release.sh release --snapshot --clean +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +resolve_flavor() { + if [ -n "${FLAVOR:-}" ]; then + echo "$FLAVOR" + return + fi + case "${GITHUB_REPOSITORY:-}" in + Datasance/potctl) echo datasance ;; + eclipse-iofog/iofogctl) echo iofog ;; + *) echo iofog ;; + esac +} + +FLAVOR="$(resolve_flavor)" + +while IFS= read -r line; do + case "$line" in + ''|\#*) continue ;; + *' ?='*) + key="${line%% ?=*}" + val="${line#* ?= }" + export "$key=$val" + ;; + esac +done < versions.mk + +case "$FLAVOR" in + datasance) + export CLI_BINARY_NAME=potctl + export CLI_CRD_GROUP=datasance.com + export CLI_API_VERSION=datasance.com/v3 + export CLI_CP_CR_NAME=pot + export IMAGE_REGISTRY=ghcr.io/datasance + export CLI_DOCS_URL=https://docs.datasance.com + export PACKAGE_REPO_BASE=https://downloads.datasance.com + export OCI_SOURCE_REPO=https://github.com/Datasance/potctl + export EDGELET_RELEASE_BASE=https://github.com/Datasance/edgelet/releases/download + export EDGELET_GITHUB_REPO=Datasance/edgelet + ;; + iofog) + export CLI_BINARY_NAME=iofogctl + export CLI_CRD_GROUP=iofog.org + export CLI_API_VERSION=iofog.org/v3 + export CLI_CP_CR_NAME=iofog + export IMAGE_REGISTRY=ghcr.io/eclipse-iofog + export CLI_DOCS_URL=https://iofog.org + export PACKAGE_REPO_BASE=https://iofog.datasance.com + export OCI_SOURCE_REPO=https://github.com/eclipse-iofog/iofogctl + export EDGELET_RELEASE_BASE=https://github.com/eclipse-iofog/edgelet/releases/download + export EDGELET_GITHUB_REPO=eclipse-iofog/edgelet + ;; + *) + echo "FLAVOR must be iofog or datasance (got: $FLAVOR)" >&2 + exit 1 + ;; +esac + +export FLAVOR + +if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + return 0 2>/dev/null || exit 0 +fi + +vars=( + FLAVOR + OPERATOR_VERSION CONTROLLER_VERSION ROUTER_VERSION NATS_VERSION + EDGELET_BINARY_VERSION EDGELET_IMAGE_TAG + CLI_BINARY_NAME CLI_CRD_GROUP CLI_API_VERSION CLI_CP_CR_NAME + IMAGE_REGISTRY CLI_DOCS_URL PACKAGE_REPO_BASE OCI_SOURCE_REPO + EDGELET_RELEASE_BASE EDGELET_GITHUB_REPO +) + +for v in "${vars[@]}"; do + printf 'export %s=%q\n' "$v" "${!v}" +done diff --git a/script/goreleaser-release.sh b/script/goreleaser-release.sh new file mode 100755 index 000000000..c1eb2bca2 --- /dev/null +++ b/script/goreleaser-release.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +eval "$("$ROOT/script/goreleaser-env.sh")" +exec goreleaser "$@" diff --git a/scripts/ci/grep-gates.sh b/scripts/ci/grep-gates.sh new file mode 100755 index 000000000..59aaf62c7 --- /dev/null +++ b/scripts/ci/grep-gates.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Fail when forbidden flavor-specific strings appear in internal/ production code. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +patterns=( + 'ghcr\.io/datasance' + 'ghcr\.io/eclipse-iofog' + 'datasance\.com/v3' + 'iofog\.org/v3' + 'downloads\.datasance\.com' + 'packagecloud\.io/iofog' + 'https://docs\.datasance\.com' + 'https://github\.com/Datasance/potctl' +) + +allowlist=( + ':!internal/**/*_test.go' + ':!internal/**/testdata/**' + ':!internal/**/fixtures/**' +) + +failed=0 +for pattern in "${patterns[@]}"; do + matches="$(git grep -En "$pattern" -- internal/ "${allowlist[@]}" 2>/dev/null || true)" + if [[ -n "$matches" ]]; then + echo "grep-gate: forbidden pattern /${pattern}/ in internal/:" >&2 + echo "$matches" >&2 + failed=1 + fi +done + +if [[ $failed -ne 0 ]]; then + echo "grep-gates: use pkg/util ldflag getters instead of hardcoded flavor strings" >&2 + exit 1 +fi + +echo "grep-gates: OK" diff --git a/scripts/vulncheck.sh b/scripts/vulncheck.sh new file mode 100755 index 000000000..cd76bc82b --- /dev/null +++ b/scripts/vulncheck.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Run govulncheck on CLI module code paths and allow documented exceptions +# listed in SECURITY.md (sync ALLOWED_VULNS with that file). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +# Documented upstream exceptions — see SECURITY.md § Known vulnerability exceptions. +ALLOWED_VULNS="" + +GOTAGS="${GOTAGS:-containers_image_openpgp,exclude_graphdriver_btrfs}" + +out="$(mktemp)" +trap 'rm -f "$out"' EXIT + +set +e +govulncheck -tags="${GOTAGS}" -format=text ./cmd/... ./internal/... ./pkg/... >"$out" 2>&1 +status=$? +set -e + +cat "$out" + +if [[ $status -eq 0 ]]; then + echo "govulncheck: no vulnerabilities affecting call paths" + exit 0 +fi + +if [[ $status -ne 3 ]]; then + echo "govulncheck: unexpected exit status $status" >&2 + exit "$status" +fi + +if [[ -z "$ALLOWED_VULNS" ]]; then + echo "govulncheck: vulnerabilities found (no exceptions configured)" >&2 + exit 3 +fi + +found="$(grep -oE 'GO-[0-9]{4}-[0-9]+' "$out" | sort -u || true)" +if [[ -z "$found" ]]; then + echo "govulncheck: failed but no GO-* IDs parsed; see output above" >&2 + exit 3 +fi + +unexpected="" +while IFS= read -r id; do + [[ -z "$id" ]] && continue + allowed=false + for a in $ALLOWED_VULNS; do + if [[ "$id" == "$a" ]]; then + allowed=true + break + fi + done + if [[ "$allowed" == false ]]; then + unexpected="${unexpected} ${id}" + fi +done <<<"$found" + +if [[ -n "${unexpected// /}" ]]; then + echo "govulncheck: unexpected vulnerabilities (not in SECURITY.md exceptions):${unexpected}" >&2 + exit 3 +fi + +echo "govulncheck: only documented exceptions remain; see SECURITY.md" +exit 0 diff --git a/test/bats/common-k8s.bats b/test/bats/common-k8s.bats index b9a119ab6..916ea34f8 100644 --- a/test/bats/common-k8s.bats +++ b/test/bats/common-k8s.bats @@ -1,9 +1,3 @@ -@test "Edge Resources" { - startTest - testEdgeResources - stopTest -} - @test "Deploy Volumes" { startTest testDeployVolume @@ -26,16 +20,6 @@ stopTest } -@test "Agent legacy commands" { - startTest - for IDX in "${!AGENTS[@]}"; do - local AGENT_NAME="${NAME}-${IDX}" - iofogctl -v -n "$NS" legacy agent "$AGENT_NAME" status - checkLegacyAgent "$AGENT_NAME" - done - stopTest -} - @test "Get Agent logs" { startTest for IDX in "${!AGENTS[@]}"; do @@ -266,27 +250,13 @@ @test "Detach with same name" { startTest - local A0="${NAME}-0" local A1="${NAME}-1" - # Rename and fail - iofogctl -v rename agent $A1 $A0 - run iofogctl -v detach agent $A0 - [ "$status" -eq 1 ] - # Rename attached and succeed - iofogctl -v rename agent $A0 $A1 iofogctl -v detach agent $A1 - # Return to attached + run iofogctl -v detach agent $A1 + [ "$status" -eq 1 ] iofogctl -v attach agent $A1 checkAgent $A1 checkDetachedAgentNegative $A1 - # Rename detached and succeed - iofogctl -v rename agent $A1 $A0 - iofogctl -v rename agent $A0 albert --detached - iofogctl -v detach agent $A0 - # Return to attached - iofogctl -v attach agent $A0 - iofogctl -v rename agent $A0 $A1 - iofogctl -v rename agent albert $A0 --detached stopTest } diff --git a/test/bats/k8s.bats b/test/bats/k8s.bats index 230c66b83..87c1ba901 100755 --- a/test/bats/k8s.bats +++ b/test/bats/k8s.bats @@ -9,8 +9,6 @@ # KEY_FILE # AGENT_PACKAGE_CLOUD_TOKEN # CONTROLLER_IMAGE -# PORT_MANAGER_IMAGE -# PROXY_IMAGE # ROUTER_IMAGE # SCHEDULER_IMAGE # OPERATOR_IMAGE @@ -19,6 +17,30 @@ NS="$NAMESPACE" +writeK8sControlPlaneYAML() { + echo "--- +apiVersion: iofog.org/v3 +kind: KubernetesControlPlane +metadata: + name: func-controlplane +spec: + iofogUser: + name: Testing + surname: Functional + email: $USER_EMAIL + password: $USER_PW + config: $KUBE_CONFIG + auth: + mode: embedded + bootstrap: + username: admin + password: BootstrapPass1! + images: + controller: $CONTROLLER_IMAGE + operator: $OPERATOR_IMAGE + router: $ROUTER_IMAGE" > test/conf/k8s.yaml +} + @test "Initialize tests" { stopTest } @@ -49,25 +71,7 @@ NS="$NAMESPACE" @test "Deploy Control Plane" { startTest - echo "--- -apiVersion: iofog.org/v3 -kind: KubernetesControlPlane -metadata: - name: func-controlplane -spec: - iofogUser: - name: Testing - surname: Functional - email: $USER_EMAIL - password: $USER_PW - config: $KUBE_CONFIG - images: - controller: $CONTROLLER_IMAGE - operator: $OPERATOR_IMAGE - portManager: $PORT_MANAGER_IMAGE - proxy: $PROXY_IMAGE - router: $ROUTER_IMAGE" > test/conf/k8s.yaml - + writeK8sControlPlaneYAML iofogctl -v -n "$NS" deploy -f test/conf/k8s.yaml checkControllerK8s stopTest @@ -75,8 +79,12 @@ spec: @test "Get endpoint" { startTest - CONTROLLER_ENDPOINT=$(iofogctl -v -n "$NS" describe controlplane | grep endpoint | sed "s|.*endpoint: ||") + DESC=$(iofogctl -v -n "$NS" describe controlplane) + CONTROLLER_ENDPOINT=$(echo "$DESC" | grep endpoint | sed "s|.*endpoint: ||") [[ ! -z "$CONTROLLER_ENDPOINT" ]] + echo "$DESC" | grep publicUrl + echo "$DESC" | grep consoleUrl + ! echo "$DESC" | grep ecnViewer echo "$CONTROLLER_ENDPOINT" > /tmp/endpoint.txt stopTest } @@ -136,25 +144,7 @@ spec: @test "Deploy Controller for idempotence" { startTest - echo "--- -apiVersion: iofog.org/v3 -kind: KubernetesControlPlane -metadata: - name: func-controlplane -spec: - iofogUser: - name: Testing - surname: Functional - email: $USER_EMAIL - password: $USER_PW - config: $KUBE_CONFIG - images: - controller: $CONTROLLER_IMAGE - operator: $OPERATOR_IMAGE - portManager: $PORT_MANAGER_IMAGE - proxy: $PROXY_IMAGE - router: $ROUTER_IMAGE" > test/conf/k8s.yaml - + writeK8sControlPlaneYAML iofogctl -v -n "$NS" deploy -f test/conf/k8s.yaml checkControllerK8s stopTest diff --git a/test/bats/local.bats b/test/bats/local.bats index de05aa532..6e79a00cd 100644 --- a/test/bats/local.bats +++ b/test/bats/local.bats @@ -34,13 +34,6 @@ NS="$NAMESPACE" stopTest } -@test "Controller legacy commands after deploy" { - startTest - iofogctl -v -n "$NS" legacy controller "$NAME" iofog list - checkLegacyController - stopTest -} - @test "Deploy Agents against local Controller" { startTest initLocalAgentFile @@ -58,25 +51,6 @@ NS="$NAMESPACE" stopTest } -@test "Edge Resources" { - startTest - testEdgeResources - stopTest -} - -@test "Agent legacy commands" { - startTest - iofogctl -v -n "$NS" legacy agent "${NAME}-0" status - checkLegacyAgent "${NAME}-0" - stopTest -} - -@test "Agent config dev mode" { - startTest - [[ ! -z $(iofogctl -v -n "$NS" legacy agent "${NAME}-0" 'config -dev on') ]] - stopTest -} - @test "Deploy local Controller again for indempotence" { startTest initLocalControllerFile @@ -156,12 +130,9 @@ NS="$NAMESPACE" stopTest } -@test "Rename and Delete Route" { +@test "Delete Route" { startTest - local NEW_ROUTE_NAME="route-2" - iofogctl -v -n "$NS" rename route $APPLICATION_NAME/"$ROUTE_NAME" "$NEW_ROUTE_NAME" - iofogctl -v -n "$NS" delete route $APPLICATION_NAME/"$NEW_ROUTE_NAME" - checkRouteNegative "$NEW_ROUTE_NAME" "$MSVC1_NAME" "$MSVC2_NAME" + iofogctl -v -n "$NS" delete route $APPLICATION_NAME/"$ROUTE_NAME" checkRouteNegative "$ROUTE_NAME" "$MSVC1_NAME" "$MSVC2_NAME" stopTest } diff --git a/test/bats/remote.bats b/test/bats/remote.bats new file mode 100644 index 000000000..770129c07 --- /dev/null +++ b/test/bats/remote.bats @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +. test/func/include.bash + +NS="$NAMESPACE" + +@test "Initialize tests" { + stopTest +} + +@test "Verify remote controller host configured" { + startTest + initVanillaController + [[ ! -z "$VANILLA_CONTROLLER" && "$VANILLA_CONTROLLER" != "user@host" ]] + stopTest +} + +@test "Create namespace" { + startTest + iofogctl create namespace "$NS" + stopTest +} + +@test "Deploy remote ControlPlane" { + startTest + initRemoteControlPlaneFile + iofogctl -v -n "$NS" deploy -f test/conf/remote-cp.yaml + checkRemoteControlPlane + stopTest +} + +@test "Describe remote controller" { + startTest + checkRemoteControlPlane + stopTest +} + +@test "Connect remote ControlPlane from file" { + startTest + iofogctl -v -n "$NS" connect -f test/conf/remote-cp.yaml + checkRemoteControlPlane + stopTest +} + +@test "Deploy add-on remote Controller" { + startTest + initRemoteControllerAddFile + iofogctl -v -n "$NS" deploy -f test/conf/remote-controller-add.yaml + [[ $(iofogctl -v -n "$NS" get controllers | grep -c "$NAME") -ge 1 ]] + stopTest +} + +@test "Delete one remote Controller" { + startTest + initRemoteControllerAddFile + local REMOTE_NAME2="${REMOTE_CONTROLLER2_NAME:-${NAME}-2}" + iofogctl -v -n "$NS" delete controller "$REMOTE_NAME2" + [[ -z $(iofogctl -v -n "$NS" get controllers | grep "$REMOTE_NAME2") ]] + [[ ! -z $(iofogctl -v -n "$NS" get controllers | grep "$NAME") ]] + initVanillaController + checkRemoteEdgeletDeleted "$REMOTE_CONTROLLER2_USER" "$REMOTE_CONTROLLER2_HOST" "${REMOTE_CONTROLLER2_PORT:-22}" "$KEY_FILE" + stopTest +} + +@test "Delete remote ControlPlane" { + startTest + initVanillaController + iofogctl -v -n "$NS" delete controlplane + checkControllerNegative + checkRemoteEdgeletDeleted "$VANILLA_USER" "$VANILLA_HOST" "$VANILLA_PORT" "$KEY_FILE" + stopTest +} + +@test "Delete namespace" { + startTest + iofogctl -v delete namespace "$NS" + [[ -z $(iofogctl get namespaces | grep "$NS") ]] + stopTest +} diff --git a/test/bats/smoke.bats b/test/bats/smoke.bats index bb4138409..775cd453b 100755 --- a/test/bats/smoke.bats +++ b/test/bats/smoke.bats @@ -34,8 +34,8 @@ iofogctl disconnect --help } -@test "Legacy Help" { - iofogctl legacy --help +@test "Move Help" { + iofogctl move --help } @test "Logs Help" { diff --git a/test/bats/vanilla.bats b/test/bats/vanilla.bats index 220ae9c1a..a7bf3d7a7 100644 --- a/test/bats/vanilla.bats +++ b/test/bats/vanilla.bats @@ -91,13 +91,6 @@ spec: stopTest } -@test "Controller legacy commands after vanilla deploy" { - startTest - iofogctl -v legacy controller "$NAME" iofog list - checkLegacyController - stopTest -} - @test "Get Controller logs after vanilla deploy" { startTest iofogctl -v logs controller "$NAME" @@ -121,12 +114,6 @@ spec: stopTest } -@test "Edge Resources" { - startTest - testEdgeResources - stopTest -} - @test "Deploy Volumes" { startTest testDeployVolume @@ -149,17 +136,6 @@ spec: stopTest } -@test "Agent legacy commands" { - startTest - initAgents - for IDX in "${!AGENTS[@]}"; do - local AGENT_NAME="${NAME}-${IDX}" - iofogctl -v legacy agent "$AGENT_NAME" status - checkLegacyAgent "$AGENT_NAME" - done - stopTest -} - @test "Prune Agent" { startTest initVanillaController @@ -186,19 +162,6 @@ spec: stopTest } -@test "Update detached agent name" { - startTest - local OLD_NAME="${NAME}-0" - local NEW_NAME="${NAME}-renamed" - iofogctl -v rename agent "$OLD_NAME" "$NEW_NAME" --detached - checkDetachedAgentNegative "$OLD_NAME" - checkDetachedAgent "$NEW_NAME" - iofogctl -v rename agent "$NEW_NAME" "$OLD_NAME" --detached - checkDetachedAgentNegative "$NEW_NAME" - checkDetachedAgent "$OLD_NAME" - stopTest -} - @test "Attach agent" { startTest local AGENT_NAME="${NAME}-0" @@ -312,10 +275,6 @@ spec: checkControllerAfterConnect "$NS2" checkAgents "$NS2" checkApplication "$NS2" - for IDX in "${!AGENTS[@]}"; do - local AGENT_NAME="${NAME}-${IDX}" - iofogctl -v -n "$NS2" legacy agent "$AGENT_NAME" status - done stopTest } @@ -377,46 +336,6 @@ spec: stopTest } -@test "Rename Agents" { - startTest - for IDX in "${!AGENTS[@]}"; do - local AGENT_NAME="${NAME}-${IDX}" - iofogctl -v -n "$NS2" rename agent "$AGENT_NAME" "newname" - checkRenamedResource agents "$AGENT_NAME" "newname" "$NS2" - iofogctl -v -n "$NS2" rename agent "newname" "$AGENT_NAME" - checkRenamedResource agents "newname" "$AGENT_NAME" "$NS2" - done - stopTest -} - -@test "Rename Controller" { - startTest - iofogctl -v -n "$NS2" rename controller "$NAME" "newname" - checkRenamedResource controllers "$NAME" "newname" "$NS2" - iofogctl -v -n "$NS2" rename controller "newname" "$NAME" - checkRenamedResource controllers "newname" "$NAME" "$NS2" - stopTest -} - -@test "Rename Namespace" { - startTest - iofogctl -v rename namespace "${NS2}" "newname" - checkRenamedNamespace "$NS2" "newname" - iofogctl -v rename namespace "newname" "${NS2}" - checkRenamedNamespace "newname" "$NS2" - stopTest -} - -@test "Rename Application" { - startTest - iofogctl -v rename application "$APPLICATION_NAME" "application-name" - iofogctl get all - checkRenamedApplication "$APPLICATION_NAME" "application-name" "$NS" - iofogctl -v rename application "application-name" "$APPLICATION_NAME" - checkRenamedApplication "application-name" "$APPLICATION_NAME" "$NS" - stopTest -} - @test "Disconnect other namespace again" { startTest iofogctl -v -n "$NS2" disconnect diff --git a/test/func/check.bash b/test/func/check.bash index 1443040df..ddc5d9761 100755 --- a/test/func/check.bash +++ b/test/func/check.bash @@ -121,6 +121,35 @@ function checkControllerNegative() { [[ "$NAME" != $(iofogctl -v -n "$NS_CHECK" get controllers | grep "$NAME" | awk '{print $1}') ]] } +function checkRemoteControlPlane() { + local NS_CHECK=${1:-$NS} + initVanillaController + [[ "$NAME" == $(iofogctl -v -n "$NS_CHECK" get controllers | grep "$NAME" | awk '{print $1}') ]] + + local DESC=$(iofogctl -v -n "$NS_CHECK" describe controller "$NAME") + echo "$DESC" | grep "name: $NAME" + echo "$DESC" | grep "host: $VANILLA_HOST" + echo "$DESC" | grep "kind: Controller" + + DESC=$(iofogctl -v -n "$NS_CHECK" describe controlplane) + echo "$DESC" | grep "host: $VANILLA_HOST" + echo "$DESC" | grep "kind: ControlPlane" +} + +function checkRemoteEdgeletDeleted() { + local USER="$1" + local HOST="$2" + local PORT="${3:-22}" + local KEY="$4" + local SSH_KEY_PATH=$KEY + if [[ ! -z $WSL_KEY_FILE ]]; then + SSH_KEY_PATH=$WSL_KEY_FILE + fi + local SSH_COMMAND="ssh -oStrictHostKeyChecking=no -p $PORT -i $SSH_KEY_PATH ${USER}@${HOST}" + run $SSH_COMMAND -- sh -c 'command -v edgelet >/dev/null 2>&1' + [ "$status" -ne 0 ] +} + function checkMicroservice() { local NS_CHECK=${1:-$NS} [[ "$MICROSERVICE_NAME" == $(iofogctl -v -n "$NS_CHECK" get microservices | grep "$MICROSERVICE_NAME" | awk '{print $1}') ]] @@ -315,9 +344,6 @@ function checkAgent() { function checkDetachedAgent() { local AGENT_NAME=$1 local NS_CHECK=${2:-$NS} - # Check agent is accessible using ssh, and is not provisioned - [[ "not" == $(iofogctl -v legacy agent $AGENT_NAME status --detached | grep 'Connection to Controller' | awk '{print $5}') ]] - # Check agent is listed in detached resources [[ "$AGENT_NAME" == $(iofogctl -v -n "$NS_CHECK" get agents --detached | grep "$AGENT_NAME" | awk '{print $1}') ]] } @@ -375,15 +401,10 @@ function checkAgentPruneController(){ [[ "true" == "$PRUNE" ]] } -function checkLegacyController() { - local NS_CHECK=${1:-$NS} - [[ ! -z $(iofogctl -v -n "$NS_CHECK" legacy controller $NAME status | grep 'ioFogController') ]] -} - function checkLegacyAgent() { local NS_CHECK=${2:-$NS} - [[ ! -z $(iofogctl -v -n "$NS_CHECK" legacy agent $1 status | grep 'RUNNING') ]] - [[ "ok" == $(iofogctl -v -n "$NS_CHECK" legacy agent $1 status | grep 'Connection to Controller' | awk '{print $5}') ]] + [[ ! -z $(iofogctl -v -n "$NS_CHECK" get agents | grep -w $1) ]] + [[ ! -z $(iofogctl -v -n "$NS_CHECK" describe agent $1 | grep -i running) ]] } function checkMovedMicroservice() { @@ -392,36 +413,11 @@ function checkMovedMicroservice() { [[ ! -z $(iofogctl -v get microservices | grep $MSVC | grep $NEW_AGENT) ]] } -function checkRenamedResource() { - local RSRC=$1 - local OLDNAME=$2 - local NEWNAME=$3 - local NAMESPACE=$4 - [[ -z $(iofogctl -n ${NAMESPACE} -v get ${RSRC} | grep -w ${OLDNAME}) ]] - [[ ! -z $(iofogctl -n ${NAMESPACE} -v get ${RSRC} | grep -w ${NEWNAME}) ]] -} - -function checkRenamedApplication() { - local OLDNAME=$1 - local NEWNAME=$2 - local NAMESPACE=$3 - - [[ -z $(iofogctl -n ${NAMESPACE} -v get applications | awk '{print $1}' | grep ${OLDNAME}) ]] - [[ ! -z $(iofogctl -n ${NAMESPACE} -v get applications | awk '{print $1}' | grep ${NEWNAME}) ]] -} - function checkNamespaceExistsNegative() { local CHECK_NS="$1" [ -z "$(iofogctl get namespaces | grep $CHECK_NS)" ] } -function checkRenamedNamespace() { - local OLDNAME=$1 - local NEWNAME=$2 - [[ -z $(iofogctl -v get namespaces | grep -w ${OLDNAME}) ]] - [[ ! -z $(iofogctl -v get namespaces | grep -w ${NEWNAME}) ]] -} - function hitMsvcEndpoint() { local PUBLIC_ENDPOINT="$1" local ITER=0 diff --git a/test/func/init.bash b/test/func/init.bash index 229e1bff8..d3b9f284f 100755 --- a/test/func/init.bash +++ b/test/func/init.bash @@ -7,6 +7,75 @@ function initVanillaController(){ VANILLA_PORT="${PORT:-22}" } +function initRemoteControlPlaneFile() { + initVanillaController + local REMOTE_ARCH="${REMOTE_ARCH:-amd64}" + local CP_PASSWORD="${REMOTE_CP_PASSWORD:-TestPassword12!}" + local AUTH_PASSWORD="${REMOTE_AUTH_PASSWORD:-LocalTest12!}" + echo "--- +apiVersion: iofog.org/v3 +kind: ControlPlane +metadata: + name: func-controlplane +spec: + iofogUser: + name: Testing + surname: Functional + email: $USER_EMAIL + password: $CP_PASSWORD + controller: + publicUrl: http://${VANILLA_HOST}:51121 + consoleUrl: http://${VANILLA_HOST} + logLevel: info + auth: + mode: embedded + insecureAllowHttp: true + insecureAllowBootstrapLog: false + bootstrap: + username: admin + password: \"$AUTH_PASSWORD\" + controllers: + - name: $NAME + host: $VANILLA_HOST + ssh: + user: $VANILLA_USER + port: $VANILLA_PORT + keyFile: $KEY_FILE + systemAgent: + config: + arch: $REMOTE_ARCH + containerEngine: edgelet + deploymentType: native" > test/conf/remote-cp.yaml +} + +function initRemoteControllerAddFile() { + initVanillaController + local REMOTE_HOST2="${REMOTE_CONTROLLER2_HOST:-}" + local REMOTE_USER2="${REMOTE_CONTROLLER2_USER:-}" + local REMOTE_PORT2="${REMOTE_CONTROLLER2_PORT:-22}" + local REMOTE_NAME2="${REMOTE_CONTROLLER2_NAME:-${NAME}-2}" + local REMOTE_ARCH="${REMOTE_ARCH:-amd64}" + if [[ -z "$REMOTE_HOST2" || -z "$REMOTE_USER2" ]]; then + skip "REMOTE_CONTROLLER2_HOST and REMOTE_CONTROLLER2_USER required for add-controller test" + fi + echo "--- +apiVersion: iofog.org/v3 +kind: Controller +metadata: + name: $REMOTE_NAME2 +spec: + host: $REMOTE_HOST2 + ssh: + user: $REMOTE_USER2 + port: $REMOTE_PORT2 + keyFile: $KEY_FILE + systemAgent: + config: + arch: $REMOTE_ARCH + containerEngine: edgelet + deploymentType: native" > test/conf/remote-controller-add.yaml +} + function initAllLocalDeleteFile() { cat test/conf/local.yaml > test/conf/all-local.yaml echo "" >> test/conf/all-local.yaml @@ -473,37 +542,6 @@ spec: " > test/conf/gcr.yaml } -function initEdgeResourceFile() { - local ER_VERSION="$EDGE_RESOURCE_VERSION" - if [ ! -z "$1" ]; then - ER_VERSION="$1" - fi - echo "--- -apiVersion: iofog.org/v3 -kind: EdgeResource -metadata: - name: $EDGE_RESOURCE_NAME -spec: - version: $ER_VERSION - description: $EDGE_RESOURCE_DESC - interfaceProtocol: $EDGE_RESOURCE_PROTOCOL - orchestrationTags: - - smart - - door - interface: - endpoints: - - name: open - method: PUT - url: '/open' - - name: close - method: PUT - url: '/close' - display: - name: 'Smart Door' - icon: 'icon' - color: '#fefefefe'" > test/conf/edge-resource.yaml -} - function initApplicationTemplateFile(){ echo -n "--- apiVersion: iofog.org/v3 diff --git a/test/func/test.bash b/test/func/test.bash index 8855a4f1a..78c282589 100755 --- a/test/func/test.bash +++ b/test/func/test.bash @@ -245,71 +245,6 @@ function testGenerateConnectionString(){ [ "$CNCT" == "iofogctl connect --ecn-addr $ADDR --name remote --email $USER_EMAIL --pass $USER_PW_B64 --b64" ] } -function testEdgeResources(){ - initEdgeResourceFile - initAgents - - # Create first version - iofogctl -n "$NS" deploy -f test/conf/edge-resource.yaml - [ ! -z "$(iofogctl -n $NS get edge-resources | grep $EDGE_RESOURCE_NAME)" ] - [ ! -z "$(iofogctl -n $NS get edge-resources | grep $EDGE_RESOURCE_VERSION)" ] - [ ! -z "$(iofogctl -n $NS get edge-resources | grep $EDGE_RESOURCE_PROTOCOL)" ] - [ ! -z "$(iofogctl -n $NS describe edge-resource $EDGE_RESOURCE_NAME $EDGE_RESOURCE_VERSION | grep "$EDGE_RESOURCE_DESC")" ] - [ ! -z "$(iofogctl -n $NS describe edge-resource $EDGE_RESOURCE_NAME $EDGE_RESOURCE_VERSION | grep $EDGE_RESOURCE_NAME)" ] - [ ! -z "$(iofogctl -n $NS describe edge-resource $EDGE_RESOURCE_NAME $EDGE_RESOURCE_VERSION | grep $EDGE_RESOURCE_VERSION)" ] - [ ! -z "$(iofogctl -n $NS describe edge-resource $EDGE_RESOURCE_NAME $EDGE_RESOURCE_VERSION | grep $EDGE_RESOURCE_PROTOCOL)" ] - # Test idempotence - iofogctl -n "$NS" deploy -f test/conf/edge-resource.yaml - - # Attach first version - local AGENT="${NAME}-0" - iofogctl -n "$NS" attach edge-resource "$EDGE_RESOURCE_NAME" "$EDGE_RESOURCE_VERSION" "$AGENT" - [ ! -z "$(iofogctl -n $NS describe agent $AGENT | grep "\- smart")" ] - [ ! -z "$(iofogctl -n $NS describe agent $AGENT | grep "\- door")" ] - - # Detach first version - iofogctl -n "$NS" detach edge-resource "$EDGE_RESOURCE_NAME" "$EDGE_RESOURCE_VERSION" "$AGENT" - [ -z "$(iofogctl -n $NS describe agent $AGENT | grep "\- smart")" ] - [ -z "$(iofogctl -n $NS describe agent $AGENT | grep "\- door")" ] - - # Deploy new version - local ER_VERS='v1.0.1' - initEdgeResourceFile "$ER_VERS" - iofogctl -n "$NS" deploy -f test/conf/edge-resource.yaml - [ ! -z "$(iofogctl -n $NS get edge-resources | grep $EDGE_RESOURCE_VERSION)" ] - [ ! -z "$(iofogctl -n $NS get edge-resources | grep $ER_VERS)" ] - - # Attach new version - iofogctl -n "$NS" attach edge-resource "$EDGE_RESOURCE_NAME" "$EDGE_RESOURCE_VERSION" "$AGENT" - [ ! -z "$(iofogctl -n $NS describe agent $AGENT | grep "\- smart")" ] - [ ! -z "$(iofogctl -n $NS describe agent $AGENT | grep "\- door")" ] - - # Rename - local NEW_NAME="smart-car" - iofogctl -n "$NS" rename edge-resource "$EDGE_RESOURCE_NAME" "$NEW_NAME" - [ -z "$(iofogctl -n $NS get edge-resources | grep $EDGE_RESOURCE_NAME)" ] - [ ! -z "$(iofogctl -n $NS get edge-resources | grep $NEW_NAME)" ] - [ ! -z "$(iofogctl -n $NS get edge-resources | grep $EDGE_RESOURCE_VERSION)" ] - [ ! -z "$(iofogctl -n $NS get edge-resources | grep $EDGE_RESOURCE_PROTOCOL)" ] - [ -z "$(iofogctl -n $NS describe edge-resource $EDGE_RESOURCE_NAME $EDGE_RESOURCE_VERSION | grep $EDGE_RESOURCE_NAME)" ] - [ ! -z "$(iofogctl -n $NS describe edge-resource "$NEW_NAME" "$EDGE_RESOURCE_VERSION" | grep "$EDGE_RESOURCE_DESC")" ] - [ ! -z "$(iofogctl -n $NS describe edge-resource "$NEW_NAME" "$EDGE_RESOURCE_VERSION" | grep $NEW_NAME)" ] - [ ! -z "$(iofogctl -n $NS describe edge-resource "$NEW_NAME" "$EDGE_RESOURCE_VERSION" | grep $EDGE_RESOURCE_VERSION)" ] - [ ! -z "$(iofogctl -n $NS describe edge-resource "$NEW_NAME" "$EDGE_RESOURCE_VERSION" | grep $EDGE_RESOURCE_PROTOCOL)" ] - iofogctl -n "$NS" rename edge-resource "$NEW_NAME" "$EDGE_RESOURCE_NAME" - [ ! -z "$(iofogctl -n $NS get edge-resources | grep $EDGE_RESOURCE_NAME)" ] - [ ! -z "$(iofogctl -n $NS describe edge-resource $EDGE_RESOURCE_NAME $EDGE_RESOURCE_VERSION | grep $EDGE_RESOURCE_NAME)" ] - - # Delete both versions - iofogctl -n "$NS" delete edge-resource "$EDGE_RESOURCE_NAME" "$EDGE_RESOURCE_VERSION" - [ -z "$(iofogctl -n $NS get edge-resources | grep $EDGE_RESOURCE_VERSION)" ] - [ ! -z "$(iofogctl -n $NS get edge-resources | grep $ER_VERS)" ] - iofogctl -n "$NS" delete edge-resource "$EDGE_RESOURCE_NAME" "$ER_VERS" - [ -z "$(iofogctl -n $NS get edge-resources | grep $ER_VERS)" ] - [ -z "$(iofogctl -n $NS describe agent $AGENT | grep "\- smart")" ] - [ -z "$(iofogctl -n $NS describe agent $AGENT | grep "\- door")" ] -} - function testApplicationTemplates(){ initApplicationTemplateFile initAgents diff --git a/test/func/vars.bash b/test/func/vars.bash index 5be189a7c..cace74616 100644 --- a/test/func/vars.bash +++ b/test/func/vars.bash @@ -14,10 +14,6 @@ USER_PW="S5gYVgLEZV" USER_PW_B64="UzVnWVZnTEVaVg==" USER_EMAIL="user@domain.com" ROUTE_NAME="route-1" -EDGE_RESOURCE_NAME="smart-door" -EDGE_RESOURCE_VERSION="v1.0.0" -EDGE_RESOURCE_DESC="Very smart door" -EDGE_RESOURCE_PROTOCOL="https" APP_TEMPLATE_NAME="app-tpl-1" APP_TEMPLATE_DESC="This is an application template to test with" APP_TEMPLATE_KEY="agent-name" diff --git a/versions.mk b/versions.mk new file mode 100644 index 000000000..24fd51557 --- /dev/null +++ b/versions.mk @@ -0,0 +1,6 @@ +OPERATOR_VERSION ?= 3.8.0-rc.1 +CONTROLLER_VERSION ?= 3.8.0-rc.5 +ROUTER_VERSION ?= 3.8.0-rc.1 +NATS_VERSION ?= 2.14.2-rc.2 +EDGELET_BINARY_VERSION ?= v1.0.0-rc.6 +EDGELET_IMAGE_TAG ?= 1.0.0-rc.6