From 43c7011d4157ce422efdf77350686ca9ba6947e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 20 Jun 2026 00:04:53 +0300 Subject: [PATCH 01/63] Establish dual-mirror build plumbing for potctl and iofogctl. Add flavor ldflags, versions.mk RC pins, go:embed assets, root NOTICE, and strip per-file copyright headers so one tree builds both binaries. --- .gitignore | 4 +- .golangci.yaml | 1 - Makefile | 65 ++- NOTICE | 11 +- assets/embed.go | 8 + cmd/iofogctl/main.go | 13 - cmd/potctl/main.go | 14 + go.mod | 6 +- go.sum | 11 - internal/attach/agent/execute.go | 13 - internal/attach/edgeresource/execute.go | 13 - internal/attach/exec/agent/execute.go | 13 - internal/attach/exec/microservice/execute.go | 13 - internal/attach/volumemount/execute.go | 13 - internal/cmd/attach.go | 13 - internal/cmd/attach_agent.go | 13 - internal/cmd/attach_edge_resource.go | 13 - internal/cmd/attach_exec.go | 13 - internal/cmd/attach_volume_moount.go | 13 - internal/cmd/bash_complete.go | 13 - internal/cmd/configure.go | 13 - internal/cmd/connect.go | 13 - internal/cmd/create.go | 13 - internal/cmd/create_namespace.go | 13 - internal/cmd/delete.go | 13 - internal/cmd/delete_agent.go | 13 - internal/cmd/delete_all.go | 13 - internal/cmd/delete_application.go | 13 - internal/cmd/delete_catalog_item.go | 13 - internal/cmd/delete_certificate.go | 13 - internal/cmd/delete_config_map.go | 13 - internal/cmd/delete_controller.go | 13 - internal/cmd/delete_edge_resource.go | 13 - internal/cmd/delete_microservice.go | 13 - internal/cmd/delete_namespace.go | 13 - internal/cmd/delete_registry.go | 13 - internal/cmd/delete_role.go | 13 - internal/cmd/delete_rolebinding.go | 13 - internal/cmd/delete_secret.go | 13 - internal/cmd/delete_service.go | 13 - internal/cmd/delete_serviceaccount.go | 13 - internal/cmd/delete_template.go | 13 - internal/cmd/delete_volume.go | 13 - internal/cmd/delete_volume_mount.go | 13 - internal/cmd/deploy.go | 13 - internal/cmd/describe.go | 13 - internal/cmd/describe_agent.go | 13 - internal/cmd/describe_agent_config.go | 13 - internal/cmd/describe_application.go | 13 - internal/cmd/describe_certificate.go | 13 - internal/cmd/describe_config_map.go | 13 - internal/cmd/describe_controller.go | 13 - internal/cmd/describe_controlplane.go | 13 - internal/cmd/describe_edge_resource.go | 13 - internal/cmd/describe_microservice.go | 13 - internal/cmd/describe_namespace.go | 13 - internal/cmd/describe_registry.go | 13 - internal/cmd/describe_role.go | 13 - internal/cmd/describe_rolebinding.go | 13 - internal/cmd/describe_secret.go | 13 - internal/cmd/describe_service.go | 13 - internal/cmd/describe_serviceaccount.go | 13 - internal/cmd/describe_system_microservice.go | 13 - internal/cmd/describe_template.go | 13 - internal/cmd/describe_volume.go | 13 - internal/cmd/describe_volume_mount.go | 13 - internal/cmd/detach.go | 13 - internal/cmd/detach_agent.go | 13 - internal/cmd/detach_edge_resource.go | 13 - internal/cmd/detach_exec.go | 13 - internal/cmd/detach_volume_mount.go | 13 - internal/cmd/disconnect.go | 13 - internal/cmd/exec.go | 13 - internal/cmd/exec_agent.go | 13 - internal/cmd/exec_microservice.go | 13 - internal/cmd/generate_documentation.go | 13 - internal/cmd/get.go | 13 - internal/cmd/legacy.go | 13 - internal/cmd/logs.go | 13 - internal/cmd/move.go | 13 - internal/cmd/move_agent.go | 13 - internal/cmd/move_microservice.go | 13 - internal/cmd/pkg.go | 13 - internal/cmd/prune.go | 13 - internal/cmd/prune_agent.go | 13 - internal/cmd/rebuild.go | 13 - internal/cmd/rebuild_microservice.go | 13 - internal/cmd/rebuild_system_microservice.go | 13 - internal/cmd/rename.go | 13 - internal/cmd/rename_agent.go | 13 - internal/cmd/rename_application.go | 13 - internal/cmd/rename_controller.go | 13 - internal/cmd/rename_edge_resource.go | 13 - internal/cmd/rename_microservice.go | 13 - internal/cmd/rename_namespace.go | 13 - internal/cmd/rollback.go | 13 - internal/cmd/root.go | 45 +- internal/cmd/start.go | 13 - internal/cmd/start_application.go | 13 - internal/cmd/start_microservice.go | 13 - internal/cmd/stop.go | 13 - internal/cmd/stop_application.go | 13 - internal/cmd/stop_microservice.go | 13 - internal/cmd/upgrade.go | 13 - internal/cmd/version.go | 19 +- internal/cmd/view.go | 13 - internal/config/config.go | 27 +- internal/config/config_test.go | 20 + internal/config/detached_agent.go | 13 - internal/config/namespace.go | 13 - internal/configure/agent.go | 13 - internal/configure/controller.go | 13 - internal/configure/controlplane.go | 13 - internal/configure/default_namespace.go | 13 - internal/configure/factory.go | 13 - internal/configure/multiple.go | 13 - internal/connect/controlplane/k8s/k8s.go | 13 - .../connect/controlplane/remote/fmt_test.go | 13 - .../connect/controlplane/remote/remote.go | 13 - internal/connect/execute.go | 13 - internal/create/namespace/namespace.go | 13 - internal/delete/agent/execute.go | 13 - internal/delete/agent/local.go | 13 - internal/delete/agent/remote.go | 13 - internal/delete/all/all.go | 13 - internal/delete/application/execute.go | 13 - internal/delete/application/legacy.go | 13 - internal/delete/application/remote.go | 13 - internal/delete/catalogitem/catalog_item.go | 13 - internal/delete/certificate/certificate.go | 13 - internal/delete/configmap/config_map.go | 13 - internal/delete/controller/execute.go | 13 - internal/delete/controller/factory.go | 13 - internal/delete/controller/local.go | 13 - internal/delete/controller/remote.go | 13 - internal/delete/controlplane/k8s/k8s.go | 13 - internal/delete/controlplane/local/local.go | 13 - internal/delete/controlplane/remote/remote.go | 13 - internal/delete/edgeresource/factory.go | 13 - internal/delete/execute.go | 13 - internal/delete/microservice/microservice.go | 13 - internal/delete/namespace/namespace.go | 13 - internal/delete/registry/registry.go | 13 - internal/delete/role/role.go | 13 - internal/delete/rolebinding/rolebinding.go | 13 - internal/delete/secret/secret.go | 13 - internal/delete/service/service.go | 13 - .../delete/serviceaccount/serviceaccount.go | 13 - internal/delete/template/execute.go | 13 - internal/delete/volume/local.go | 13 - internal/delete/volume/remote.go | 13 - internal/delete/volume/volume.go | 13 - internal/delete/volumemount/volume_mount.go | 13 - internal/deploy/agent/execute.go | 13 - internal/deploy/agent/factory.go | 13 - internal/deploy/agent/local.go | 13 - internal/deploy/agent/remote.go | 13 - internal/deploy/agentconfig/factory.go | 13 - internal/deploy/agentconfig/utils.go | 13 - internal/deploy/application/factory.go | 13 - .../deploy/applicationtemplate/factory.go | 13 - internal/deploy/catalogitem/catalog_item.go | 13 - internal/deploy/certificate/factory.go | 13 - internal/deploy/configmap/factory.go | 13 - internal/deploy/controller/local/local.go | 13 - internal/deploy/controller/remote/remote.go | 13 - internal/deploy/controlplane/k8s/execute.go | 13 - internal/deploy/controlplane/local/execute.go | 13 - .../deploy/controlplane/remote/execute.go | 13 - internal/deploy/edgeresource/factory.go | 13 - internal/deploy/execute.go | 13 - internal/deploy/microservice/factory.go | 13 - internal/deploy/registry/factory.go | 13 - internal/deploy/role/factory.go | 13 - internal/deploy/rolebinding/factory.go | 13 - internal/deploy/secret/factory.go | 13 - internal/deploy/service/factory.go | 13 - internal/deploy/serviceaccount/factory.go | 13 - internal/deploy/volume/factory.go | 13 - internal/deploy/volume/local.go | 13 - internal/deploy/volume/remote.go | 13 - internal/deploy/volumeMount/factory.go | 13 - internal/describe/agent.go | 13 - internal/describe/agent_config.go | 13 - internal/describe/application.go | 13 - internal/describe/application_legacy.go | 13 - internal/describe/certificate.go | 13 - internal/describe/config_map.go | 13 - internal/describe/controller.go | 13 - internal/describe/controlplane.go | 13 - internal/describe/edge_resource.go | 13 - internal/describe/factory.go | 13 - internal/describe/microservice.go | 13 - internal/describe/namespace.go | 13 - internal/describe/registry.go | 13 - internal/describe/role.go | 13 - internal/describe/rolebinding.go | 13 - internal/describe/secret.go | 13 - internal/describe/service.go | 13 - internal/describe/serviceaccount.go | 13 - internal/describe/system_microservice.go | 13 - internal/describe/template.go | 13 - internal/describe/utils.go | 13 - internal/describe/volume.go | 13 - internal/describe/volume_mount.go | 13 - internal/detach/agent/execute.go | 13 - internal/detach/agent/local.go | 13 - internal/detach/agent/remote.go | 13 - internal/detach/edgeresource/execute.go | 13 - internal/detach/exec/agent/execute.go | 13 - internal/detach/exec/microservice/execute.go | 13 - internal/detach/volumemount/execute.go | 13 - internal/disconnect/disconnect.go | 13 - internal/exec/agent.go | 13 - internal/exec/factory.go | 13 - internal/exec/microservice.go | 13 - internal/exec/utils.go | 13 - internal/execute/executor.go | 13 - internal/execute/parallel.go | 13 - internal/execute/utils.go | 13 - internal/get/agents.go | 13 - internal/get/all.go | 13 - internal/get/applications.go | 13 - internal/get/applications_legacy.go | 13 - internal/get/catalog.go | 13 - internal/get/certificates.go | 13 - internal/get/config_maps.go | 13 - internal/get/controllers.go | 13 - internal/get/edge_resources.go | 13 - internal/get/factory.go | 13 - internal/get/microservices.go | 13 - internal/get/namespaces.go | 13 - internal/get/print.go | 13 - internal/get/registry.go | 13 - internal/get/role.go | 13 - internal/get/rolebinding.go | 13 - internal/get/secrets.go | 13 - internal/get/serviceaccount.go | 13 - internal/get/services.go | 13 - internal/get/system_applications.go | 13 - internal/get/system_microservices.go | 13 - internal/get/templates.go | 13 - internal/get/util.go | 13 - internal/get/util_test.go | 13 - internal/get/volume_mounts.go | 13 - internal/get/volumes.go | 13 - internal/logs/agent.go | 13 - internal/logs/config.go | 13 - internal/logs/factory.go | 13 - internal/logs/k8s_controller.go | 13 - internal/logs/local_controller.go | 13 - internal/logs/microservice.go | 13 - internal/logs/remote_controller.go | 13 - internal/logs/stream.go | 13 - internal/logs/utils.go | 13 - internal/move/microservice/executor.go | 13 - internal/prune/agent/execute.go | 13 - internal/prune/agent/local.go | 13 - internal/prune/agent/remote.go | 13 - internal/rebuild/microservice/execute.go | 13 - .../rebuild/systemmicroservice/execute.go | 13 - internal/rename/agent/executor.go | 13 - internal/rename/application/executor.go | 13 - internal/rename/controller/executor.go | 13 - internal/rename/edgeresource/executor.go | 13 - internal/rename/microservice/executor.go | 13 - internal/rename/namespace/executor.go | 13 - internal/resource/agent.go | 13 - internal/resource/controller.go | 13 - internal/resource/controlplane.go | 13 - internal/resource/controlplane_test.go | 13 - internal/resource/k8s_controller.go | 13 - internal/resource/k8s_controlplane.go | 13 - internal/resource/local_agent.go | 13 - internal/resource/local_controller.go | 13 - internal/resource/local_controlplane.go | 13 - internal/resource/remote_agent.go | 13 - internal/resource/remote_controller.go | 13 - internal/resource/remote_controlplane.go | 13 - internal/resource/types.go | 13 - internal/resource/user.go | 13 - internal/rollback/agent.go | 13 - internal/rollback/factory.go | 13 - internal/start/application/application.go | 13 - internal/start/microservice/microservice.go | 13 - internal/stop/application/application.go | 13 - internal/stop/microservice/microservice.go | 13 - internal/upgrade/agent.go | 13 - internal/upgrade/factory.go | 13 - internal/util/client/api.go | 13 - internal/util/client/client.go | 13 - internal/util/client/pkg.go | 13 - internal/util/misc.go | 13 - internal/util/terminal/terminal.go | 13 - internal/util/terminal/terminal_windows.go | 13 - internal/util/update_openid_client.go | 13 - internal/util/websocket/client.go | 13 - internal/util/websocket/constants.go | 13 - internal/util/websocket/message.go | 13 - internal/util/websocket/session.go | 13 - pkg/iofog/constants.go | 13 - pkg/iofog/install/agent.go | 13 - pkg/iofog/install/controller.go | 13 - pkg/iofog/install/k8s.go | 13 - pkg/iofog/install/k8s_microservices.go | 13 - pkg/iofog/install/k8s_util.go | 13 - pkg/iofog/install/local_agent.go | 13 - pkg/iofog/install/local_container.go | 13 - pkg/iofog/install/pkg.go | 13 - pkg/iofog/install/remote_agent.go | 13 - pkg/iofog/install/remote_agent_test.go | 13 - pkg/iofog/install/types.go | 13 - pkg/iofog/install/verbosity.go | 13 - pkg/util/assets.go | 37 +- pkg/util/debug.go | 13 - pkg/util/errors.go | 13 - pkg/util/exec.go | 13 - pkg/util/get_controller_endpoint.go | 13 - pkg/util/get_controller_endpoint_test.go | 12 - pkg/util/is_local.go | 13 - pkg/util/is_system_msvc.go | 13 - pkg/util/print.go | 13 - pkg/util/rand.go | 13 - pkg/util/rice-box.go | 420 ------------------ pkg/util/ssh.go | 13 - pkg/util/strings.go | 13 - pkg/util/time.go | 13 - pkg/util/url.go | 13 - pkg/util/version.go | 67 +-- pkg/util/yaml.go | 13 - script/bootstrap.sh | 9 - versions.mk | 5 + 332 files changed, 195 insertions(+), 4668 deletions(-) create mode 100644 assets/embed.go create mode 100644 cmd/potctl/main.go create mode 100644 internal/config/config_test.go delete mode 100644 pkg/util/rice-box.go create mode 100644 versions.mk 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..e84480d59 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -127,7 +127,6 @@ linters-settings: - github.com/docker - k8s.io - sigs.k8s.io/controller-runtime - - github.com/GeertJohan/go.rice - github.com/briandowns/spinner dupl: threshold: 100 diff --git a/Makefile b/Makefile index d0a1f670d..34ff7151e 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,18 +24,42 @@ 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 + IMAGE_REGISTRY = ghcr.io/datasance + CLI_DOCS_URL = https://docs.datasance.com + PACKAGE_REPO_BASE = downloads.datasance.com + OCI_SOURCE_REPO = https://github.com/Datasance/potctl +else + CLI_BINARY_NAME = iofogctl + CLI_CRD_GROUP = iofog.org + CLI_API_VERSION = iofog.org/v3 + IMAGE_REGISTRY = ghcr.io/eclipse-iofog + CLI_DOCS_URL = https://iofog.org + PACKAGE_REPO_BASE = https://packagecloud.io/iofog + OCI_SOURCE_REPO = https://github.com/eclipse-iofog/iofogctl +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).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_VERSION) +LDFLAGS += -X $(PREFIX).controllerVersion=$(CONTROLLER_VERSION) +LDFLAGS += -X $(PREFIX).edgeletVersion=$(EDGELET_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 TEST_RESULTS ?= TEST-iofogctl.txt TEST_REPORT ?= TEST-iofogctl.xml @@ -49,15 +79,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 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/assets/embed.go b/assets/embed.go new file mode 100644 index 000000000..14a000660 --- /dev/null +++ b/assets/embed.go @@ -0,0 +1,8 @@ +package assets + +import "embed" + +// FS holds install scripts and service unit templates bundled with the CLI. +// +//go:embed agent airgap-agent airgap-controller container-agent container-controller controller +var FS embed.FS 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/go.mod b/go.mod index e134bac05..7e353909f 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,8 @@ 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 @@ -50,7 +47,6 @@ require ( 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/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 diff --git a/go.sum b/go.sum index 8801b7c4b..9fbc44642 100644 --- a/go.sum +++ b/go.sum @@ -8,9 +8,6 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg6 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= 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= @@ -19,7 +16,6 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o 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= @@ -55,9 +51,6 @@ github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f h 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/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= @@ -185,7 +178,6 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l 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= @@ -242,7 +234,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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= @@ -328,8 +319,6 @@ github.com/twmb/algoimpl v0.0.0-20170717182524-076353e90b94 h1:RVeQNVS7eoXqFemL1 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= 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 index 81b0eec97..27ae7a068 100644 --- a/internal/attach/edgeresource/execute.go +++ b/internal/attach/edgeresource/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 attachedgeresource import ( diff --git a/internal/attach/exec/agent/execute.go b/internal/attach/exec/agent/execute.go index 2d621ca5e..f38335213 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 ( diff --git a/internal/attach/exec/microservice/execute.go b/internal/attach/exec/microservice/execute.go index 11a7f506c..2e3061cf6 100644 --- a/internal/attach/exec/microservice/execute.go +++ b/internal/attach/exec/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 attachexecmicroservice import ( 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/cmd/attach.go b/internal/cmd/attach.go index bda74da67..dda76a8a4 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 ( diff --git a/internal/cmd/attach_agent.go b/internal/cmd/attach_agent.go index 57f3a2316..5e9789371 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 ( diff --git a/internal/cmd/attach_edge_resource.go b/internal/cmd/attach_edge_resource.go index cdb4c0b03..bed2aa59f 100644 --- a/internal/cmd/attach_edge_resource.go +++ b/internal/cmd/attach_edge_resource.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/attach_exec.go b/internal/cmd/attach_exec.go index ea2f532bb..c01b8569b 100644 --- a/internal/cmd/attach_exec.go +++ b/internal/cmd/attach_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/attach_volume_moount.go b/internal/cmd/attach_volume_moount.go index 1d5cbd019..a479a9a70 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 ( diff --git a/internal/cmd/bash_complete.go b/internal/cmd/bash_complete.go index 5d149bbbb..b1fc67cac 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 ( diff --git a/internal/cmd/configure.go b/internal/cmd/configure.go index 487fb47bd..2d7231ba5 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 ( diff --git a/internal/cmd/connect.go b/internal/cmd/connect.go index 9ce7ef837..88ad26b6e 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 ( 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..40615b725 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 ( diff --git a/internal/cmd/delete.go b/internal/cmd/delete.go index 7019fd3c6..ef70a4921 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 ( diff --git a/internal/cmd/delete_agent.go b/internal/cmd/delete_agent.go index 95cfc2a3f..276c80cb8 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 ( diff --git a/internal/cmd/delete_all.go b/internal/cmd/delete_all.go index 5fa831ab4..e6498d100 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 ( diff --git a/internal/cmd/delete_application.go b/internal/cmd/delete_application.go index 3d70acacd..eac49a299 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 ( diff --git a/internal/cmd/delete_catalog_item.go b/internal/cmd/delete_catalog_item.go index 134580c70..b719da4f8 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 ( diff --git a/internal/cmd/delete_certificate.go b/internal/cmd/delete_certificate.go index 7ca8b831a..62d20b66b 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 ( diff --git a/internal/cmd/delete_config_map.go b/internal/cmd/delete_config_map.go index 3a041dbb1..24ca9724d 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 ( diff --git a/internal/cmd/delete_controller.go b/internal/cmd/delete_controller.go index 6a370894d..b4d7b8696 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 ( diff --git a/internal/cmd/delete_edge_resource.go b/internal/cmd/delete_edge_resource.go index 347bf5cb1..b28e784d9 100644 --- a/internal/cmd/delete_edge_resource.go +++ b/internal/cmd/delete_edge_resource.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/delete_microservice.go b/internal/cmd/delete_microservice.go index 686595309..f505c1b84 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 ( diff --git a/internal/cmd/delete_namespace.go b/internal/cmd/delete_namespace.go index b8ab8749a..e5892b6d4 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 ( diff --git a/internal/cmd/delete_registry.go b/internal/cmd/delete_registry.go index b36ba90b4..4ccfab39d 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 ( diff --git a/internal/cmd/delete_role.go b/internal/cmd/delete_role.go index 2cb7a6699..120b8ab6b 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 ( diff --git a/internal/cmd/delete_rolebinding.go b/internal/cmd/delete_rolebinding.go index a14273f2f..a7ac6690f 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 ( diff --git a/internal/cmd/delete_secret.go b/internal/cmd/delete_secret.go index 00de7e35a..3ad22197e 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 ( diff --git a/internal/cmd/delete_service.go b/internal/cmd/delete_service.go index cb37f7540..447a3e5d7 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 ( diff --git a/internal/cmd/delete_serviceaccount.go b/internal/cmd/delete_serviceaccount.go index 72b99e142..76b5c345b 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 ( diff --git a/internal/cmd/delete_template.go b/internal/cmd/delete_template.go index e3ac73d8b..1f6285454 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 ( diff --git a/internal/cmd/delete_volume.go b/internal/cmd/delete_volume.go index 8be642751..de23bb522 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 ( diff --git a/internal/cmd/delete_volume_mount.go b/internal/cmd/delete_volume_mount.go index 2586448d2..95a706cbc 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 ( diff --git a/internal/cmd/deploy.go b/internal/cmd/deploy.go index 3f729a05f..e51d8672d 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 ( diff --git a/internal/cmd/describe.go b/internal/cmd/describe.go index 78aaf4e0f..036d4d68e 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 ( diff --git a/internal/cmd/describe_agent.go b/internal/cmd/describe_agent.go index ed3750132..3f095438e 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 ( diff --git a/internal/cmd/describe_agent_config.go b/internal/cmd/describe_agent_config.go index 536febb84..2d7a5b639 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 ( diff --git a/internal/cmd/describe_application.go b/internal/cmd/describe_application.go index cdaf1738d..7c1000970 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 ( diff --git a/internal/cmd/describe_certificate.go b/internal/cmd/describe_certificate.go index ac0b44986..749a50e73 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 ( diff --git a/internal/cmd/describe_config_map.go b/internal/cmd/describe_config_map.go index f11ab017c..686647676 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 ( diff --git a/internal/cmd/describe_controller.go b/internal/cmd/describe_controller.go index 899f70233..cc76efe72 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 ( diff --git a/internal/cmd/describe_controlplane.go b/internal/cmd/describe_controlplane.go index 73c5b8b6a..1539feef7 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 ( diff --git a/internal/cmd/describe_edge_resource.go b/internal/cmd/describe_edge_resource.go index 634463f89..0ea7abe7b 100644 --- a/internal/cmd/describe_edge_resource.go +++ b/internal/cmd/describe_edge_resource.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/describe_microservice.go b/internal/cmd/describe_microservice.go index 535383c72..0e49c8c97 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 ( diff --git a/internal/cmd/describe_namespace.go b/internal/cmd/describe_namespace.go index 7387be6b8..172284d22 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 ( diff --git a/internal/cmd/describe_registry.go b/internal/cmd/describe_registry.go index a3f655308..6e6eba9e0 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 ( diff --git a/internal/cmd/describe_role.go b/internal/cmd/describe_role.go index a8767df94..27e886549 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 ( diff --git a/internal/cmd/describe_rolebinding.go b/internal/cmd/describe_rolebinding.go index 8775cabf2..c38969110 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 ( diff --git a/internal/cmd/describe_secret.go b/internal/cmd/describe_secret.go index 3d1ed99e6..8c14636e1 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 ( diff --git a/internal/cmd/describe_service.go b/internal/cmd/describe_service.go index 042187c09..a35d67a26 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 ( diff --git a/internal/cmd/describe_serviceaccount.go b/internal/cmd/describe_serviceaccount.go index 2a4011524..d3af0cbc1 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 ( diff --git a/internal/cmd/describe_system_microservice.go b/internal/cmd/describe_system_microservice.go index ecc6be6fa..828d78253 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 ( diff --git a/internal/cmd/describe_template.go b/internal/cmd/describe_template.go index f613f4207..ed18feff3 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 ( diff --git a/internal/cmd/describe_volume.go b/internal/cmd/describe_volume.go index a74f44adc..22f4088d0 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 ( diff --git a/internal/cmd/describe_volume_mount.go b/internal/cmd/describe_volume_mount.go index 78f68ac66..83587a255 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 ( diff --git a/internal/cmd/detach.go b/internal/cmd/detach.go index 1069a6b43..35236e61f 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 ( diff --git a/internal/cmd/detach_agent.go b/internal/cmd/detach_agent.go index 5a72ccfce..da46fb3b7 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 ( diff --git a/internal/cmd/detach_edge_resource.go b/internal/cmd/detach_edge_resource.go index 16a74a7e3..fbe66c971 100644 --- a/internal/cmd/detach_edge_resource.go +++ b/internal/cmd/detach_edge_resource.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/detach_exec.go b/internal/cmd/detach_exec.go index 33a44332d..e2818df91 100644 --- a/internal/cmd/detach_exec.go +++ b/internal/cmd/detach_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/detach_volume_mount.go b/internal/cmd/detach_volume_mount.go index c49111396..953d53fa5 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 ( diff --git a/internal/cmd/disconnect.go b/internal/cmd/disconnect.go index 653ff8673..9ce4a66fd 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 ( 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..cea8a3ee2 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 ( diff --git a/internal/cmd/exec_microservice.go b/internal/cmd/exec_microservice.go index e6170ecbe..15870dace 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 ( diff --git a/internal/cmd/generate_documentation.go b/internal/cmd/generate_documentation.go index 5da5e3c7c..cf6914890 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 ( diff --git a/internal/cmd/get.go b/internal/cmd/get.go index 122dff0cb..b1a8dbb73 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 ( diff --git a/internal/cmd/legacy.go b/internal/cmd/legacy.go index f2541fffc..96dcabf35 100644 --- a/internal/cmd/legacy.go +++ b/internal/cmd/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 cmd import ( diff --git a/internal/cmd/logs.go b/internal/cmd/logs.go index 4b94942a5..c1cd64b20 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 ( 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..eeb04b045 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 ( diff --git a/internal/cmd/move_microservice.go b/internal/cmd/move_microservice.go index 1182d9cbb..ff3d8f080 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 ( diff --git a/internal/cmd/pkg.go b/internal/cmd/pkg.go index edea218c7..a8ef4ce88 100644 --- a/internal/cmd/pkg.go +++ b/internal/cmd/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 cmd import ( 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..be53d7a6d 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 ( 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..ce186291f 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 ( diff --git a/internal/cmd/rebuild_system_microservice.go b/internal/cmd/rebuild_system_microservice.go index 1375af8ad..6bd592502 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 ( diff --git a/internal/cmd/rename.go b/internal/cmd/rename.go index bc29bbf35..dbc380ff3 100644 --- a/internal/cmd/rename.go +++ b/internal/cmd/rename.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/rename_agent.go b/internal/cmd/rename_agent.go index a8ce89eef..506604a57 100644 --- a/internal/cmd/rename_agent.go +++ b/internal/cmd/rename_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 ( diff --git a/internal/cmd/rename_application.go b/internal/cmd/rename_application.go index cdf498fa4..8d9783957 100644 --- a/internal/cmd/rename_application.go +++ b/internal/cmd/rename_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 ( diff --git a/internal/cmd/rename_controller.go b/internal/cmd/rename_controller.go index 1e0dd37b9..f04f8fd7e 100644 --- a/internal/cmd/rename_controller.go +++ b/internal/cmd/rename_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 ( diff --git a/internal/cmd/rename_edge_resource.go b/internal/cmd/rename_edge_resource.go index 1e2099a40..0ebf8b825 100644 --- a/internal/cmd/rename_edge_resource.go +++ b/internal/cmd/rename_edge_resource.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/rename_microservice.go b/internal/cmd/rename_microservice.go index e7b361182..fff4b822d 100644 --- a/internal/cmd/rename_microservice.go +++ b/internal/cmd/rename_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 ( diff --git a/internal/cmd/rename_namespace.go b/internal/cmd/rename_namespace.go index c1641be09..25f9f5845 100644 --- a/internal/cmd/rename_namespace.go +++ b/internal/cmd/rename_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 ( diff --git a/internal/cmd/rollback.go b/internal/cmd/rollback.go index 287d1f8de..c4b201bb9 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 ( diff --git a/internal/cmd/root.go b/internal/cmd/root.go index e67f56ea4..72b5686ce 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") 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..d6ddd85a1 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 ( diff --git a/internal/cmd/start_microservice.go b/internal/cmd/start_microservice.go index 69cbee79d..2b7ab8202 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 ( 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..2974490b3 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 ( diff --git a/internal/cmd/stop_microservice.go b/internal/cmd/stop_microservice.go index 9f351e2c4..38d1ae982 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 ( diff --git a/internal/cmd/upgrade.go b/internal/cmd/upgrade.go index 123ebc122..7f21d7bc9 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 ( 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..77f7ba19f 100644 --- a/internal/cmd/view.go +++ b/internal/cmd/view.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/config/config.go b/internal/config/config.go index 9f569d3bb..d293b26dc 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,10 +21,13 @@ var ( namespaces map[string]*rsc.Namespace ) +var ( + apiVersionGroup = "iofog.org" + LatestAPIVersion = "iofog.org/v3" +) + const ( - apiVersionGroup = "iofog.org" latestVersion = "v3" - LatestAPIVersion = apiVersionGroup + "/" + latestVersion defaultDirname = ".iofog/" + latestVersion namespaceDirname = "namespaces/" offlineImagesDirname = "offline-images" @@ -48,6 +38,11 @@ const ( 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) @@ -256,7 +251,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..f8f5859a8 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 ( diff --git a/internal/config/namespace.go b/internal/config/namespace.go index 1a95ea1ef..73dba9fcc 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 ( 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..e970ed84d 100644 --- a/internal/configure/controlplane.go +++ b/internal/configure/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 configure import ( 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..bb19ea53e 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 ( 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/k8s/k8s.go b/internal/connect/controlplane/k8s/k8s.go index 244b5f13c..bb8ce5967 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 ( 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..689b249f5 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 ( diff --git a/internal/connect/execute.go b/internal/connect/execute.go index 21dcf1308..a8dd11245 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 ( 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..c84ea3375 100644 --- a/internal/delete/agent/execute.go +++ b/internal/delete/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 deleteagent import ( diff --git a/internal/delete/agent/local.go b/internal/delete/agent/local.go index 8853c7672..5312966b1 100644 --- a/internal/delete/agent/local.go +++ b/internal/delete/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 deleteagent import ( diff --git a/internal/delete/agent/remote.go b/internal/delete/agent/remote.go index b79fe8958..eaf30204f 100644 --- a/internal/delete/agent/remote.go +++ b/internal/delete/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 deleteagent import ( diff --git a/internal/delete/all/all.go b/internal/delete/all/all.go index 760606c3d..222b3830e 100644 --- a/internal/delete/all/all.go +++ b/internal/delete/all/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 deleteall import ( 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..3ee557ca0 100644 --- a/internal/delete/application/legacy.go +++ b/internal/delete/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 deleteapplication func (exe *Executor) initLegacy() (err error) { diff --git a/internal/delete/application/remote.go b/internal/delete/application/remote.go index 3d157c546..6c556622d 100644 --- a/internal/delete/application/remote.go +++ b/internal/delete/application/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 deleteapplication import ( diff --git a/internal/delete/catalogitem/catalog_item.go b/internal/delete/catalogitem/catalog_item.go index f5cb69008..c514a07b4 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 ( diff --git a/internal/delete/certificate/certificate.go b/internal/delete/certificate/certificate.go index b1e93d084..4e543f46f 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 ( diff --git a/internal/delete/configmap/config_map.go b/internal/delete/configmap/config_map.go index 75149e179..b343942e1 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 ( 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..8d45d29bd 100644 --- a/internal/delete/controller/local.go +++ b/internal/delete/controller/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 deletecontroller import ( diff --git a/internal/delete/controller/remote.go b/internal/delete/controller/remote.go index 4569ce01b..46cdff622 100644 --- a/internal/delete/controller/remote.go +++ b/internal/delete/controller/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 deletecontroller import ( diff --git a/internal/delete/controlplane/k8s/k8s.go b/internal/delete/controlplane/k8s/k8s.go index 85f70426a..3dcb13c4e 100644 --- a/internal/delete/controlplane/k8s/k8s.go +++ b/internal/delete/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 deletek8scontrolplane import ( diff --git a/internal/delete/controlplane/local/local.go b/internal/delete/controlplane/local/local.go index d9ad445a2..564b7baaa 100644 --- a/internal/delete/controlplane/local/local.go +++ b/internal/delete/controlplane/local/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 deletelocalcontrolplane import ( diff --git a/internal/delete/controlplane/remote/remote.go b/internal/delete/controlplane/remote/remote.go index f57255f46..d22a23128 100644 --- a/internal/delete/controlplane/remote/remote.go +++ b/internal/delete/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 deleteremotecontrolplane import ( diff --git a/internal/delete/edgeresource/factory.go b/internal/delete/edgeresource/factory.go index 11fe694b5..baa700a9e 100644 --- a/internal/delete/edgeresource/factory.go +++ b/internal/delete/edgeresource/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 deleteedgeresource import ( diff --git a/internal/delete/execute.go b/internal/delete/execute.go index a406be410..abde4c94a 100644 --- a/internal/delete/execute.go +++ b/internal/delete/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 delete import ( diff --git a/internal/delete/microservice/microservice.go b/internal/delete/microservice/microservice.go index a001cf228..8fe5352b7 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 ( diff --git a/internal/delete/namespace/namespace.go b/internal/delete/namespace/namespace.go index 3cdd34980..0ac14b58c 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 ( diff --git a/internal/delete/registry/registry.go b/internal/delete/registry/registry.go index 2eafdc5f5..687f21fa0 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 ( 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..f60fb6713 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 ( diff --git a/internal/delete/service/service.go b/internal/delete/service/service.go index 4a1939baa..f7cb1dfc7 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 ( 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..39189a601 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 ( 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..f23182091 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 ( diff --git a/internal/delete/volume/volume.go b/internal/delete/volume/volume.go index 5ef54237c..27bce8c80 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 ( diff --git a/internal/delete/volumemount/volume_mount.go b/internal/delete/volumemount/volume_mount.go index 9ded5ee39..b74f442b5 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 ( 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..34a79b98d 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 ( diff --git a/internal/deploy/agent/local.go b/internal/deploy/agent/local.go index 6b9332f8f..6c7b5fad4 100644 --- a/internal/deploy/agent/local.go +++ b/internal/deploy/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 deployagent import ( diff --git a/internal/deploy/agent/remote.go b/internal/deploy/agent/remote.go index bb6049356..e1a87cb08 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 ( diff --git a/internal/deploy/agentconfig/factory.go b/internal/deploy/agentconfig/factory.go index b9ccd0173..8de31b436 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 ( diff --git a/internal/deploy/agentconfig/utils.go b/internal/deploy/agentconfig/utils.go index 22b561e4e..901eb4ac9 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 ( 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..3c1a5fb58 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 ( 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..343d8858d 100644 --- a/internal/deploy/controller/local/local.go +++ b/internal/deploy/controller/local/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 deploylocalcontroller import ( diff --git a/internal/deploy/controller/remote/remote.go b/internal/deploy/controller/remote/remote.go index cb9693ca1..2fd788f98 100644 --- a/internal/deploy/controller/remote/remote.go +++ b/internal/deploy/controller/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 deployremotecontroller import ( diff --git a/internal/deploy/controlplane/k8s/execute.go b/internal/deploy/controlplane/k8s/execute.go index f59a47339..cbdd7a035 100644 --- a/internal/deploy/controlplane/k8s/execute.go +++ b/internal/deploy/controlplane/k8s/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 deployk8scontrolplane import ( diff --git a/internal/deploy/controlplane/local/execute.go b/internal/deploy/controlplane/local/execute.go index ec4161f3b..3123e7053 100644 --- a/internal/deploy/controlplane/local/execute.go +++ b/internal/deploy/controlplane/local/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 deploylocalcontrolplane import ( diff --git a/internal/deploy/controlplane/remote/execute.go b/internal/deploy/controlplane/remote/execute.go index bc3977951..d9d5f1c80 100644 --- a/internal/deploy/controlplane/remote/execute.go +++ b/internal/deploy/controlplane/remote/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 deployremotecontrolplane import ( diff --git a/internal/deploy/edgeresource/factory.go b/internal/deploy/edgeresource/factory.go index de5c91b31..be1afb6bd 100644 --- a/internal/deploy/edgeresource/factory.go +++ b/internal/deploy/edgeresource/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 deployedgeresource import ( diff --git a/internal/deploy/execute.go b/internal/deploy/execute.go index 42b57bda2..b6d83ba37 100644 --- a/internal/deploy/execute.go +++ b/internal/deploy/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 deploy import ( 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/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..ae8e8847b 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 ( 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/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..504e2cb85 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 ( diff --git a/internal/deploy/volume/remote.go b/internal/deploy/volume/remote.go index 81d0b813d..c2f222e61 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 ( 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/application.go b/internal/describe/application.go index ea3832e3c..d4e8181e8 100644 --- a/internal/describe/application.go +++ b/internal/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 describe import ( diff --git a/internal/describe/application_legacy.go b/internal/describe/application_legacy.go index 49c67cf6f..891696841 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 ( 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..9da4a8725 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 ( 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 index b164a7d81..c63601220 100644 --- a/internal/describe/edge_resource.go +++ b/internal/describe/edge_resource.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/factory.go b/internal/describe/factory.go index a823efedf..5ea9164de 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 ( 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..c967979e0 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 ( 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/utils.go b/internal/describe/utils.go index 3e44889af..0498414cd 100644 --- a/internal/describe/utils.go +++ b/internal/describe/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 describe import ( 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..a62bc0679 100644 --- a/internal/detach/agent/execute.go +++ b/internal/detach/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 detachagent import ( diff --git a/internal/detach/agent/local.go b/internal/detach/agent/local.go index 03ae30689..f3decd6d8 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 ( diff --git a/internal/detach/agent/remote.go b/internal/detach/agent/remote.go index 37e242866..f54a2ee0c 100644 --- a/internal/detach/agent/remote.go +++ b/internal/detach/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 detachagent import ( diff --git a/internal/detach/edgeresource/execute.go b/internal/detach/edgeresource/execute.go index b2f218d6a..fe98e445f 100644 --- a/internal/detach/edgeresource/execute.go +++ b/internal/detach/edgeresource/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 detachedgeresource import ( diff --git a/internal/detach/exec/agent/execute.go b/internal/detach/exec/agent/execute.go index 1bc4aadd2..bf4c5281f 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 ( diff --git a/internal/detach/exec/microservice/execute.go b/internal/detach/exec/microservice/execute.go index 152b2d26e..1d18dbfb3 100644 --- a/internal/detach/exec/microservice/execute.go +++ b/internal/detach/exec/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 detachexecmicroservice import ( 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..64f527b15 100644 --- a/internal/exec/agent.go +++ b/internal/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 exec import ( diff --git a/internal/exec/factory.go b/internal/exec/factory.go index f77c75889..55c98863a 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 ( diff --git a/internal/exec/microservice.go b/internal/exec/microservice.go index f126a4b5e..3452ac469 100644 --- a/internal/exec/microservice.go +++ b/internal/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 exec import ( diff --git a/internal/exec/utils.go b/internal/exec/utils.go index 4466a2bd7..43f1f3eb2 100644 --- a/internal/exec/utils.go +++ b/internal/exec/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 exec import ( 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..ddac095b5 100644 --- a/internal/execute/utils.go +++ b/internal/execute/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 execute import ( 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..b12a7c6a1 100644 --- a/internal/get/all.go +++ b/internal/get/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 get import ( diff --git a/internal/get/applications.go b/internal/get/applications.go index c5599c9b2..3a7a9af80 100644 --- a/internal/get/applications.go +++ b/internal/get/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 ( diff --git a/internal/get/applications_legacy.go b/internal/get/applications_legacy.go index 8b00bf59a..7a44cf93a 100644 --- a/internal/get/applications_legacy.go +++ b/internal/get/applications_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 get import "github.com/eclipse-iofog/iofogctl/pkg/util" diff --git a/internal/get/catalog.go b/internal/get/catalog.go index 5ad59abb9..a3f0198c9 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 ( 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..11b62fe45 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 ( diff --git a/internal/get/edge_resources.go b/internal/get/edge_resources.go index f85d24bef..ea65fcf16 100644 --- a/internal/get/edge_resources.go +++ b/internal/get/edge_resources.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/factory.go b/internal/get/factory.go index 8835f6019..3a3cadb12 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 ( 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..94dc50e94 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 ( diff --git a/internal/get/system_applications.go b/internal/get/system_applications.go index ff13fa01d..7ab9f169d 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 ( 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..83a9dd872 100644 --- a/internal/logs/agent.go +++ b/internal/logs/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 logs import ( diff --git a/internal/logs/config.go b/internal/logs/config.go index 3969da1e9..1234702b8 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 ( 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..9034bc9bf 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 ( diff --git a/internal/logs/microservice.go b/internal/logs/microservice.go index 6a79241bf..2dcfc3edc 100644 --- a/internal/logs/microservice.go +++ b/internal/logs/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 logs import ( diff --git a/internal/logs/remote_controller.go b/internal/logs/remote_controller.go index eb7d1ee68..292315061 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 ( diff --git a/internal/logs/stream.go b/internal/logs/stream.go index e92928d7e..041d58420 100644 --- a/internal/logs/stream.go +++ b/internal/logs/stream.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/utils.go b/internal/logs/utils.go index 96a49a138..2ed35b08d 100644 --- a/internal/logs/utils.go +++ b/internal/logs/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 logs import ( diff --git a/internal/move/microservice/executor.go b/internal/move/microservice/executor.go index 469a7dc54..27884c4df 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 ( diff --git a/internal/prune/agent/execute.go b/internal/prune/agent/execute.go index f53dec4c4..5c2faf11f 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 ( diff --git a/internal/prune/agent/local.go b/internal/prune/agent/local.go index 703df2d95..92e9f8879 100644 --- a/internal/prune/agent/local.go +++ b/internal/prune/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 pruneagent import ( diff --git a/internal/prune/agent/remote.go b/internal/prune/agent/remote.go index 34c2585c7..384e5f758 100644 --- a/internal/prune/agent/remote.go +++ b/internal/prune/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 pruneagent import ( 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/rename/agent/executor.go b/internal/rename/agent/executor.go index fc8efb3fc..c95999d7f 100644 --- a/internal/rename/agent/executor.go +++ b/internal/rename/agent/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 agent import ( diff --git a/internal/rename/application/executor.go b/internal/rename/application/executor.go index 9588baee1..6f676abd0 100644 --- a/internal/rename/application/executor.go +++ b/internal/rename/application/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 application import ( diff --git a/internal/rename/controller/executor.go b/internal/rename/controller/executor.go index 7e33ae743..dd073b505 100644 --- a/internal/rename/controller/executor.go +++ b/internal/rename/controller/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 controller import ( diff --git a/internal/rename/edgeresource/executor.go b/internal/rename/edgeresource/executor.go index 2fd9c7f0f..d6ba62d15 100644 --- a/internal/rename/edgeresource/executor.go +++ b/internal/rename/edgeresource/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 edgeresource import ( diff --git a/internal/rename/microservice/executor.go b/internal/rename/microservice/executor.go index d368ad854..4fab18328 100644 --- a/internal/rename/microservice/executor.go +++ b/internal/rename/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 microservice import ( diff --git a/internal/rename/namespace/executor.go b/internal/rename/namespace/executor.go index 52a76c51d..2af342199 100644 --- a/internal/rename/namespace/executor.go +++ b/internal/rename/namespace/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 namespace import ( 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/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..94aec6fa7 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 { diff --git a/internal/resource/controlplane_test.go b/internal/resource/controlplane_test.go index 17d98399d..3dc4525e6 100644 --- a/internal/resource/controlplane_test.go +++ b/internal/resource/controlplane_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 resource import ( 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..0c21d7933 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 ( diff --git a/internal/resource/local_agent.go b/internal/resource/local_agent.go index 4d1464be2..eb06b0135 100644 --- a/internal/resource/local_agent.go +++ b/internal/resource/local_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 LocalAgent struct { 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..e693ed220 100644 --- a/internal/resource/local_controlplane.go +++ b/internal/resource/local_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 ( 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..27839a994 100644 --- a/internal/resource/remote_controller.go +++ b/internal/resource/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 resource import ( diff --git a/internal/resource/remote_controlplane.go b/internal/resource/remote_controlplane.go index 0dd495050..4d58252b8 100644 --- a/internal/resource/remote_controlplane.go +++ b/internal/resource/remote_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 ( diff --git a/internal/resource/types.go b/internal/resource/types.go index 4b6b7fc5e..24495de06 100644 --- a/internal/resource/types.go +++ b/internal/resource/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 resource import ( 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/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..215ed1ba3 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 ( 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..3b3bdaf89 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 ( 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/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..735831f14 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 ( diff --git a/internal/util/client/client.go b/internal/util/client/client.go index edd88d20d..34d3c02b6 100644 --- a/internal/util/client/client.go +++ b/internal/util/client/client.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 ( diff --git a/internal/util/client/pkg.go b/internal/util/client/pkg.go index 9594d8e8e..db3bdeb20 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 ( 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 index d2808b638..e4e78a8c6 100644 --- a/internal/util/terminal/terminal.go +++ b/internal/util/terminal/terminal.go @@ -1,19 +1,6 @@ //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 ( diff --git a/internal/util/terminal/terminal_windows.go b/internal/util/terminal/terminal_windows.go index db91beb17..addac0777 100644 --- a/internal/util/terminal/terminal_windows.go +++ b/internal/util/terminal/terminal_windows.go @@ -1,19 +1,6 @@ //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 ( diff --git a/internal/util/update_openid_client.go b/internal/util/update_openid_client.go index cf7cbc96e..b943bae6a 100644 --- a/internal/util/update_openid_client.go +++ b/internal/util/update_openid_client.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/websocket/client.go b/internal/util/websocket/client.go index af957eb6f..861d99fa0 100644 --- a/internal/util/websocket/client.go +++ b/internal/util/websocket/client.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 websocket import ( diff --git a/internal/util/websocket/constants.go b/internal/util/websocket/constants.go index 326a38dff..23620af55 100644 --- a/internal/util/websocket/constants.go +++ b/internal/util/websocket/constants.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 websocket // Message types for WebSocket communication diff --git a/internal/util/websocket/message.go b/internal/util/websocket/message.go index 9fd33fde9..8ec9b1ef9 100644 --- a/internal/util/websocket/message.go +++ b/internal/util/websocket/message.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 websocket import ( diff --git a/internal/util/websocket/session.go b/internal/util/websocket/session.go index ae5c8414f..a2865dfe7 100644 --- a/internal/util/websocket/session.go +++ b/internal/util/websocket/session.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 websocket import ( diff --git a/pkg/iofog/constants.go b/pkg/iofog/constants.go index 7a342e824..eecf038d5 100644 --- a/pkg/iofog/constants.go +++ b/pkg/iofog/constants.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 iofog import "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" diff --git a/pkg/iofog/install/agent.go b/pkg/iofog/install/agent.go index a7333d88f..27d436e22 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 ( diff --git a/pkg/iofog/install/controller.go b/pkg/iofog/install/controller.go index e696f79f4..8e3f9f9fb 100644 --- a/pkg/iofog/install/controller.go +++ b/pkg/iofog/install/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 install import ( diff --git a/pkg/iofog/install/k8s.go b/pkg/iofog/install/k8s.go index 3c907542b..753079b24 100644 --- a/pkg/iofog/install/k8s.go +++ b/pkg/iofog/install/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 install import ( diff --git a/pkg/iofog/install/k8s_microservices.go b/pkg/iofog/install/k8s_microservices.go index d35e22324..b064d34fa 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 ( diff --git a/pkg/iofog/install/k8s_util.go b/pkg/iofog/install/k8s_util.go index 4e8c1e751..6c415fb54 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 ( diff --git a/pkg/iofog/install/local_agent.go b/pkg/iofog/install/local_agent.go index 04256e74d..f327f4bb1 100644 --- a/pkg/iofog/install/local_agent.go +++ b/pkg/iofog/install/local_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 ( diff --git a/pkg/iofog/install/local_container.go b/pkg/iofog/install/local_container.go index c1cb30903..67538085d 100644 --- a/pkg/iofog/install/local_container.go +++ b/pkg/iofog/install/local_container.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/iofog/install/pkg.go b/pkg/iofog/install/pkg.go index 50cdf5119..4da0a349d 100644 --- a/pkg/iofog/install/pkg.go +++ b/pkg/iofog/install/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 install var pkg struct { diff --git a/pkg/iofog/install/remote_agent.go b/pkg/iofog/install/remote_agent.go index 6f0a7e0e4..85e60e87a 100644 --- a/pkg/iofog/install/remote_agent.go +++ b/pkg/iofog/install/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 install import ( diff --git a/pkg/iofog/install/remote_agent_test.go b/pkg/iofog/install/remote_agent_test.go index 88fc4cc16..1b00121cd 100644 --- a/pkg/iofog/install/remote_agent_test.go +++ b/pkg/iofog/install/remote_agent_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 install import ( diff --git a/pkg/iofog/install/types.go b/pkg/iofog/install/types.go index f0bfdba91..ae9030d5d 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 ( 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/errors.go b/pkg/util/errors.go index 5c97e7f78..520554bec 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 ( diff --git a/pkg/util/exec.go b/pkg/util/exec.go index b1fae0c8a..66aa72590 100644 --- a/pkg/util/exec.go +++ b/pkg/util/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 util import ( 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/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/print.go b/pkg/util/print.go index ff5546276..b875438f9 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 ( diff --git a/pkg/util/rand.go b/pkg/util/rand.go index c69f123ec..7de5f3b1d 100644 --- a/pkg/util/rand.go +++ b/pkg/util/rand.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/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/ssh.go b/pkg/util/ssh.go index f92b2aa0f..a8b89a7e8 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 ( 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..618b579bd 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,27 @@ var ( commit = "undefined" date = "undefined" - repo = "undefined" + cliBinaryName = "iofogctl" + cliCrdGroup = "iofog.org" + cliApiVersion = "iofog.org/v3" + imageRegistry = "ghcr.io/eclipse-iofog" + cliDocsUrl = "https://iofog.org" + packageRepoBase = "https://packagecloud.io/iofog" + ociSourceRepo = "https://github.com/eclipse-iofog/iofogctl" controllerTag = "undefined" - agentTag = "undefined" operatorTag = "undefined" routerTag = "undefined" natsTag = "undefined" + edgeletTag = "undefined" controllerVersion = "undefined" - agentVersion = "undefined" + edgeletVersion = "undefined" debuggerTag = "undefined" ) const ( controllerImage = "controller" - agentImage = "agent" + edgeletImage = "edgelet" operatorImage = "operator" routerImage = "router" routerARMImage = "router" @@ -60,15 +53,39 @@ func GetVersion() Version { } } +func GetCliBinaryName() string { return cliBinaryName } +func GetCliCrdGroup() string { return cliCrdGroup } +func GetCliApiVersion() string { return cliApiVersion } +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 GetAgentVersion() string { return agentVersion } +func GetEdgeletVersion() string { return edgeletVersion } 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 GetRouterImage() string { + return fmt.Sprintf("%s/%s:%s", imageRegistry, routerImage, routerTag) } -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 GetRouterARMImage() string { + return fmt.Sprintf("%s/%s:%s", imageRegistry, routerARMImage, 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..8942d4184 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 ( diff --git a/script/bootstrap.sh b/script/bootstrap.sh index 2416cd270..1068c2de2 100755 --- a/script/bootstrap.sh +++ b/script/bootstrap.sh @@ -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'" diff --git a/versions.mk b/versions.mk new file mode 100644 index 000000000..586331c8d --- /dev/null +++ b/versions.mk @@ -0,0 +1,5 @@ +OPERATOR_VERSION ?= 3.8.0-rc.1 +CONTROLLER_VERSION ?= 3.8.0-rc.1 +ROUTER_VERSION ?= 3.8.0-rc.1 +NATS_VERSION ?= 2.14.2-rc.1 +EDGELET_VERSION ?= 3.8.0-rc.2 From da3eb658784e981e27887a6c1f237e2a7c579f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 20 Jun 2026 03:44:49 +0300 Subject: [PATCH 02/63] Retire Azure Pipelines and legacy CI workflow. Remove azure-pipelines.yaml, the pipeline/ test templates, and the old GitHub Actions ci.yaml. --- .github/workflows/ci.yaml | 152 ----------------- azure-pipelines.yaml | 188 --------------------- pipeline/ha.yaml | 67 -------- pipeline/k8s.yaml | 56 ------ pipeline/local.yaml | 41 ----- pipeline/steps/configure-env.sh | 17 -- pipeline/steps/configure-remote-tests.yaml | 35 ---- pipeline/steps/functional-clean-vm.yaml | 25 --- pipeline/steps/functional-post-test.yaml | 16 -- pipeline/steps/init-gcloud-steps.yaml | 22 --- pipeline/steps/init-ssh.yaml | 10 -- pipeline/steps/init-vms.yaml | 83 --------- pipeline/steps/install-test-deps.yaml | 5 - pipeline/steps/postinstall.yaml | 5 - pipeline/steps/prebuild.yaml | 26 --- pipeline/steps/publish-deps.yaml | 18 -- pipeline/steps/vanilla.yaml | 44 ----- pipeline/steps/version.yaml | 15 -- pipeline/vanilla.yaml | 20 --- pipeline/win-k8s.yaml | 106 ------------ pipeline/win-local.yaml | 104 ------------ pipeline/win-vanilla.yaml | 88 ---------- 22 files changed, 1143 deletions(-) delete mode 100644 .github/workflows/ci.yaml delete mode 100644 azure-pipelines.yaml delete mode 100644 pipeline/ha.yaml delete mode 100644 pipeline/k8s.yaml delete mode 100644 pipeline/local.yaml delete mode 100755 pipeline/steps/configure-env.sh delete mode 100644 pipeline/steps/configure-remote-tests.yaml delete mode 100644 pipeline/steps/functional-clean-vm.yaml delete mode 100644 pipeline/steps/functional-post-test.yaml delete mode 100644 pipeline/steps/init-gcloud-steps.yaml delete mode 100644 pipeline/steps/init-ssh.yaml delete mode 100644 pipeline/steps/init-vms.yaml delete mode 100644 pipeline/steps/install-test-deps.yaml delete mode 100644 pipeline/steps/postinstall.yaml delete mode 100644 pipeline/steps/prebuild.yaml delete mode 100644 pipeline/steps/publish-deps.yaml delete mode 100644 pipeline/steps/vanilla.yaml delete mode 100644 pipeline/steps/version.yaml delete mode 100644 pipeline/vanilla.yaml delete mode 100644 pipeline/win-k8s.yaml delete mode 100644 pipeline/win-local.yaml delete mode 100644 pipeline/win-vanilla.yaml 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/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/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 From cafaa39cedc3655d17719f1c82c33b840bb326ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 20 Jun 2026 03:44:54 +0300 Subject: [PATCH 03/63] Add GitHub Actions CI and security workflows. PR CI builds both flavors on Datasance and iofogctl only on Eclipse via repository conditionals; add CodeQL and govulncheck. --- .github/workflows/ci.yml | 152 ++++++++++++++++++++++++++++++ .github/workflows/codeql.yml | 34 +++++++ .github/workflows/govulncheck.yml | 28 ++++++ 3 files changed, 214 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/govulncheck.yml 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..31c933490 --- /dev/null +++ b/.github/workflows/govulncheck.yml @@ -0,0 +1,28 @@ +name: govulncheck + +on: + push: + paths: + - go.sum + - go.mod + schedule: + - cron: '0 6 * * *' + workflow_dispatch: + +permissions: read-all + +env: + GO_VERSION: '1.26.4' + +jobs: + govulncheck: + name: govulncheck + 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: Run govulncheck + run: make vulncheck From 852abed4ff870ca2a935cd57e3735a784668bf2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 20 Jun 2026 03:44:59 +0300 Subject: [PATCH 04/63] Add CI scripts and Makefile targets for lint and security checks. Introduce grep-gates and vulncheck scripts plus test-unit, smoke, security-code, and golangci-lint v2.12.2 install. --- Makefile | 37 ++++++++++++++++++++++-- scripts/ci/grep-gates.sh | 40 ++++++++++++++++++++++++++ scripts/vulncheck.sh | 61 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) create mode 100755 scripts/ci/grep-gates.sh create mode 100755 scripts/vulncheck.sh diff --git a/Makefile b/Makefile index 34ff7151e..38acdbd70 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,9 @@ LDFLAGS += -X $(PREFIX).controllerVersion=$(CONTROLLER_VERSION) LDFLAGS += -X $(PREFIX).edgeletVersion=$(EDGELET_VERSION) LDFLAGS += -X $(PREFIX).debuggerTag=latest -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 @@ -104,7 +106,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 @@ -115,6 +117,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 + @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 $(GOSEC_SCOPE) + .PHONY: test test: ## Run unit tests mkdir -p $(REPORTS_DIR) 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..fcc5ee798 --- /dev/null +++ b/scripts/vulncheck.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +ALLOWED_VULNS="" + +out="$(mktemp)" +trap 'rm -f "$out"' EXIT + +set +e +govulncheck -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:${unexpected}" >&2 + exit 3 +fi + +echo "govulncheck: only documented exceptions remain" +exit 0 From 5a2a8560cfb5670594727b5f35fb12218191a0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 20 Jun 2026 03:45:03 +0300 Subject: [PATCH 05/63] Migrate golangci-lint configuration to v2. Align with edgelet linter set and add depguard import allowlist for iofogctl module dependencies. --- .golangci.yaml | 365 +++++++++++++++++++------------------------------ 1 file changed, 137 insertions(+), 228 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index e84480d59..1fec21bfe 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,234 +1,143 @@ +# 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/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: + - ^\.cursor/ + - ^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 + - 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/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: + - ^\.cursor/ + - ^vendor/ + presets: + - comments + - common-false-positives + - legacy + - std-error-handling +run: + timeout: 5m From 8ad38e66da2688f55a90209676efeada4b7bcdd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 20 Jun 2026 03:45:08 +0300 Subject: [PATCH 06/63] Stop hardcoding iofog.org/v3 in internal YAML and config paths. Initialize LatestAPIVersion from ldflags and use util.GetCliApiVersion() for manifest apiVersion fields. --- internal/cmd/nats_rules_common.go | 2 +- internal/config/config.go | 4 ++-- internal/move/microservice/executor.go | 2 +- internal/rename/microservice/executor.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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/config/config.go b/internal/config/config.go index d293b26dc..af811085a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -22,8 +22,8 @@ var ( ) var ( - apiVersionGroup = "iofog.org" - LatestAPIVersion = "iofog.org/v3" + apiVersionGroup string + LatestAPIVersion string ) const ( diff --git a/internal/move/microservice/executor.go b/internal/move/microservice/executor.go index 27884c4df..50d78056f 100644 --- a/internal/move/microservice/executor.go +++ b/internal/move/microservice/executor.go @@ -51,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/rename/microservice/executor.go b/internal/rename/microservice/executor.go index 4fab18328..ff71d2de8 100644 --- a/internal/rename/microservice/executor.go +++ b/internal/rename/microservice/executor.go @@ -43,7 +43,7 @@ func Execute(namespace, name, newName 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}, "/"), From 85974072fdee053374195f32905cfd327d772bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 20 Jun 2026 03:45:14 +0300 Subject: [PATCH 07/63] Fix golangci-lint and go vet findings across the codebase. Correct error handling, formatting, and ignored return values; add CI badges and contribution notes for GitHub Actions. --- CONTRIBUTING | 9 ++ README.md | 9 ++ internal/attach/exec/agent/execute.go | 6 +- internal/cmd/configure.go | 2 +- internal/cmd/rollback.go | 2 +- internal/cmd/upgrade.go | 2 +- internal/cmd/view.go | 1 + internal/delete/application/remote.go | 5 +- internal/delete/controller/remote.go | 1 + internal/delete/execute.go | 4 +- internal/deploy/agent/remote.go | 2 +- internal/deploy/agentconfig/factory.go | 2 +- internal/deploy/agentconfig/utils.go | 20 ++-- internal/deploy/airgap/helpers.go | 2 +- internal/deploy/airgap/transfer.go | 2 +- internal/deploy/controller/remote/remote.go | 6 +- internal/deploy/controlplane/local/execute.go | 8 +- .../deploy/controlplane/remote/execute.go | 112 +++++++++--------- internal/deploy/execute.go | 4 +- internal/deploy/natsaccountrule/factory.go | 6 +- internal/deploy/natsuserrule/factory.go | 6 +- internal/deploy/offlineimage/executor.go | 2 +- internal/deploy/offlineimage/images.go | 2 +- internal/deploy/volume/local.go | 2 +- internal/describe/application.go | 5 +- internal/describe/config_map.go | 6 +- internal/describe/utils.go | 9 +- internal/detach/exec/agent/execute.go | 6 +- internal/exec/agent.go | 3 +- internal/exec/utils.go | 17 +-- internal/execute/utils.go | 3 +- internal/get/applications.go | 4 +- internal/logs/config.go | 2 +- internal/resource/controlplane_test.go | 5 +- internal/resource/namespace_test.go | 4 +- internal/resource/types.go | 2 +- internal/util/client/client.go | 18 +-- internal/util/terminal/terminal.go | 8 +- internal/util/update_openid_client.go | 18 +-- internal/util/websocket/client.go | 14 ++- internal/util/websocket/message.go | 2 +- pkg/iofog/constants.go | 2 +- pkg/iofog/install/controller.go | 2 +- pkg/iofog/install/k8s.go | 11 +- pkg/iofog/install/local_container.go | 6 +- pkg/iofog/install/remote_agent.go | 2 +- pkg/util/errors.go | 2 +- pkg/util/print.go | 8 +- pkg/util/spinner.go | 2 +- 49 files changed, 199 insertions(+), 179 deletions(-) diff --git a/CONTRIBUTING b/CONTRIBUTING index 89d72e8c3..e2ca5f038 100644 --- a/CONTRIBUTING +++ b/CONTRIBUTING @@ -34,6 +34,15 @@ contributions are always welcome! Please run `make init` in order to initialise git hooks +## 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` + ## Eclipse Contributor Agreement Before your contribution can be accepted by the project team contributors must diff --git a/README.md b/README.md index 3698c109d..9ff8df727 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ ![iofogctl-logo](iofogctl-logo.png?raw=true "iofogctl logo") +[![CLI CI](https://github.com/Datasance/potctl/actions/workflows/ci.yml/badge.svg)](https://github.com/Datasance/potctl/actions/workflows/ci.yml) +[![Release](https://img.shields.io/github/v/release/Datasance/potctl?include_prereleases)](https://github.com/Datasance/potctl/releases) +[![Go](https://img.shields.io/badge/Go-1.26.4-blue)](https://go.dev/) +[![License](https://img.shields.io/badge/License-EPL--2.0-blue.svg)](LICENSE) +[![govulncheck](https://github.com/Datasance/potctl/actions/workflows/govulncheck.yml/badge.svg)](https://github.com/Datasance/potctl/actions/workflows/govulncheck.yml) +[![CodeQL](https://github.com/Datasance/potctl/actions/workflows/codeql.yml/badge.svg)](https://github.com/Datasance/potctl/actions/workflows/codeql.yml) + +Canonical development: [Datasance/potctl](https://github.com/Datasance/potctl) · Upstream mirror: [eclipse-iofog/iofogctl](https://github.com/eclipse-iofog/iofogctl) + `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 diff --git a/internal/attach/exec/agent/execute.go b/internal/attach/exec/agent/execute.go index f38335213..6d58f24f8 100644 --- a/internal/attach/exec/agent/execute.go +++ b/internal/attach/exec/agent/execute.go @@ -44,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 @@ -55,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/cmd/configure.go b/internal/cmd/configure.go index 2d7231ba5..8c887f075 100644 --- a/internal/cmd/configure.go +++ b/internal/cmd/configure.go @@ -55,7 +55,7 @@ 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") diff --git a/internal/cmd/rollback.go b/internal/cmd/rollback.go index c4b201bb9..d194f07d8 100644 --- a/internal/cmd/rollback.go +++ b/internal/cmd/rollback.go @@ -37,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/upgrade.go b/internal/cmd/upgrade.go index 7f21d7bc9..91f79d4f0 100644 --- a/internal/cmd/upgrade.go +++ b/internal/cmd/upgrade.go @@ -37,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/view.go b/internal/cmd/view.go index 77f7ba19f..4752f91af 100644 --- a/internal/cmd/view.go +++ b/internal/cmd/view.go @@ -29,6 +29,7 @@ func newViewCommand() *cobra.Command { os.Exit(1) } cp, err := ns.GetControlPlane() + util.Check(err) cpEndpoint, err := cp.GetEndpoint() if err != nil { util.PrintError("Failed to get Control Plane endpoint: " + err.Error()) diff --git a/internal/delete/application/remote.go b/internal/delete/application/remote.go index 6c556622d..d4afd392b 100644 --- a/internal/delete/application/remote.go +++ b/internal/delete/application/remote.go @@ -1,6 +1,8 @@ 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" @@ -45,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/controller/remote.go b/internal/delete/controller/remote.go index 46cdff622..d96e80576 100644 --- a/internal/delete/controller/remote.go +++ b/internal/delete/controller/remote.go @@ -5,6 +5,7 @@ import ( "github.com/eclipse-iofog/iofogctl/internal/config" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + // "github.com/eclipse-iofog/iofogctl/pkg/iofog" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" diff --git a/internal/delete/execute.go b/internal/delete/execute.go index abde4c94a..54b5a4b08 100644 --- a/internal/delete/execute.go +++ b/internal/delete/execute.go @@ -1,6 +1,7 @@ package delete import ( + "errors" "fmt" "github.com/eclipse-iofog/iofogctl/internal/config" @@ -142,7 +143,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/deploy/agent/remote.go b/internal/deploy/agent/remote.go index e1a87cb08..657d7a92a 100644 --- a/internal/deploy/agent/remote.go +++ b/internal/deploy/agent/remote.go @@ -219,7 +219,7 @@ func (exe *remoteExecutor) Execute() (err error) { return fmt.Errorf("failed to resolve platform: %w", err) } - engine, err := deployairgap.ResolveContainerEngine(exe.agent.Config.AgentConfiguration.ContainerEngine) + engine, err := deployairgap.ResolveContainerEngine(exe.agent.Config.ContainerEngine) if err != nil { return fmt.Errorf("failed to resolve container engine: %w", err) } diff --git a/internal/deploy/agentconfig/factory.go b/internal/deploy/agentconfig/factory.go index 8de31b436..14553ecbf 100644 --- a/internal/deploy/agentconfig/factory.go +++ b/internal/deploy/agentconfig/factory.go @@ -100,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 diff --git a/internal/deploy/agentconfig/utils.go b/internal/deploy/agentconfig/utils.go index 901eb4ac9..266bd05e1 100644 --- a/internal/deploy/agentconfig/utils.go +++ b/internal/deploy/agentconfig/utils.go @@ -23,15 +23,15 @@ 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 } @@ -52,11 +52,11 @@ func Validate(config *rsc.AgentConfiguration) error { 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.RouterConfig.EdgeRouterPort != nil || config.RouterConfig.InterRouterPort != nil) { + 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.NatsConfig.NatsClusterPort != nil) { + 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)) } @@ -149,12 +149,12 @@ func createAgentFromConfiguration(agentConfig *rsc.AgentConfiguration, tags *[]s createAgentRequest := &client.CreateAgentRequest{ AgentUpdateRequest: updateAgentConfigRequest, } - if createAgentRequest.AgentUpdateRequest.Name == "" { - createAgentRequest.AgentUpdateRequest.Name = name + if createAgentRequest.Name == "" { + createAgentRequest.Name = name } - if createAgentRequest.AgentUpdateRequest.FogType == nil { + if createAgentRequest.FogType == nil { fogType := int64(0) - createAgentRequest.AgentUpdateRequest.FogType = &fogType + createAgentRequest.FogType = &fogType } agent, err := clt.CreateAgent(createAgentRequest) if err != nil { diff --git a/internal/deploy/airgap/helpers.go b/internal/deploy/airgap/helpers.go index 1e29e0818..5b08580e6 100644 --- a/internal/deploy/airgap/helpers.go +++ b/internal/deploy/airgap/helpers.go @@ -87,7 +87,7 @@ func ValidateAirgapRequirements(agentConfig *rsc.AgentConfiguration) error { } // Validate ContainerEngine - if agentConfig.AgentConfiguration.ContainerEngine == nil || *agentConfig.AgentConfiguration.ContainerEngine == "" { + if agentConfig.ContainerEngine == nil || *agentConfig.ContainerEngine == "" { return util.NewInputError("ContainerEngine is required for airgap deployment. Please specify the container engine (docker or podman)") } diff --git a/internal/deploy/airgap/transfer.go b/internal/deploy/airgap/transfer.go index 1c1ba69d9..d97e13c69 100644 --- a/internal/deploy/airgap/transfer.go +++ b/internal/deploy/airgap/transfer.go @@ -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{ diff --git a/internal/deploy/controller/remote/remote.go b/internal/deploy/controller/remote/remote.go index 2fd788f98..98921e17a 100644 --- a/internal/deploy/controller/remote/remote.go +++ b/internal/deploy/controller/remote/remote.go @@ -186,10 +186,8 @@ func (exe *remoteExecutor) Execute() (err error) { return } // Update controller - useHTTPS := false - if exe.controller.Https != nil && exe.controller.Https.Enabled != nil && *exe.controller.Https.Enabled { - useHTTPS = true - } + useHTTPS := exe.controller.Https != nil && exe.controller.Https.Enabled != nil && *exe.controller.Https.Enabled + exe.controller.Endpoint, err = util.GetControllerEndpoint(exe.controller.Host, useHTTPS) if err != nil { return err diff --git a/internal/deploy/controlplane/local/execute.go b/internal/deploy/controlplane/local/execute.go index 3123e7053..ff24ac24d 100644 --- a/internal/deploy/controlplane/local/execute.go +++ b/internal/deploy/controlplane/local/execute.go @@ -54,7 +54,7 @@ func prepareViewerURL(endpoint string) (string, error) { if err != nil || URL.Host == "" { URL, err = url.Parse("//" + endpoint) if err != nil { - return "", fmt.Errorf("failed to parse endpoint: %v", err) + return "", fmt.Errorf("failed to parse endpoint: %w", err) } } @@ -66,7 +66,7 @@ func prepareViewerURL(endpoint string) (string, error) { 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) + return "", fmt.Errorf("failed to split host and port: %w", err) } } else { host = URL.Host @@ -92,12 +92,12 @@ func updateViewerClientRootURL(controlPlane *rsc.LocalControlPlane, endpoint str // Prepare viewer URL viewerURL, err := prepareViewerURL(endpoint) if err != nil { - return fmt.Errorf("failed to prepare viewer URL: %v", err) + return fmt.Errorf("failed to prepare viewer URL: %w", 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 fmt.Errorf("failed to update viewer client root URL: %w", err) } return nil diff --git a/internal/deploy/controlplane/remote/execute.go b/internal/deploy/controlplane/remote/execute.go index d9d5f1c80..158ba7228 100644 --- a/internal/deploy/controlplane/remote/execute.go +++ b/internal/deploy/controlplane/remote/execute.go @@ -39,32 +39,32 @@ const ( // 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) + if cfg.NatsMode == nil { + cfg.NatsMode = iutil.MakeStrPtr(iofog.NatsModeServer) } else { // Force server mode for system agents (like router interior) - cfg.NatsConfig.NatsMode = iutil.MakeStrPtr(iofog.NatsModeServer) + cfg.NatsMode = iutil.MakeStrPtr(iofog.NatsModeServer) } - if cfg.NatsConfig.NatsServerPort == nil { - cfg.NatsConfig.NatsServerPort = iutil.MakeIntPtr(defaultNatsServerPort) + if cfg.NatsServerPort == nil { + cfg.NatsServerPort = iutil.MakeIntPtr(defaultNatsServerPort) } - if cfg.NatsConfig.NatsClusterPort == nil { - cfg.NatsConfig.NatsClusterPort = iutil.MakeIntPtr(defaultNatsClusterPort) + if cfg.NatsClusterPort == nil { + cfg.NatsClusterPort = iutil.MakeIntPtr(defaultNatsClusterPort) } - if cfg.NatsConfig.NatsLeafPort == nil { - cfg.NatsConfig.NatsLeafPort = iutil.MakeIntPtr(defaultNatsLeafPort) + if cfg.NatsLeafPort == nil { + cfg.NatsLeafPort = iutil.MakeIntPtr(defaultNatsLeafPort) } - if cfg.NatsConfig.NatsMqttPort == nil { + if cfg.NatsMqttPort == nil { cfg.NatsMqttPort = iutil.MakeIntPtr(defaultNatsMqttPort) } - if cfg.NatsConfig.NatsHttpPort == nil { - cfg.NatsConfig.NatsHttpPort = iutil.MakeIntPtr(defaultNatsHttpPort) + if cfg.NatsHttpPort == nil { + cfg.NatsHttpPort = iutil.MakeIntPtr(defaultNatsHttpPort) } - if cfg.NatsConfig.JsStorageSize == nil { - cfg.NatsConfig.JsStorageSize = iutil.MakeStrPtr(defaultJsStorageSize) + if cfg.JsStorageSize == nil { + cfg.JsStorageSize = iutil.MakeStrPtr(defaultJsStorageSize) } - if cfg.NatsConfig.JsMemoryStoreSize == nil { - cfg.NatsConfig.JsMemoryStoreSize = iutil.MakeStrPtr(defaultJsMemoryStoreSize) + if cfg.JsMemoryStoreSize == nil { + cfg.JsMemoryStoreSize = iutil.MakeStrPtr(defaultJsMemoryStoreSize) } } @@ -87,9 +87,9 @@ func deploySystemAgent(namespace string, ctrl *rsc.RemoteController, systemAgent 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 { + if systemAgentConfig != nil && systemAgentConfig.AgentConfiguration != nil && systemAgentConfig.AgentConfiguration.DeploymentType != nil { // Use DeploymentType from provided configuration - deploymentType = *systemAgentConfig.AgentConfiguration.AgentConfiguration.DeploymentType + deploymentType = *systemAgentConfig.AgentConfiguration.DeploymentType } else if systemAgentConfig != nil && systemAgentConfig.Package.Container.Image != "" { // If container image is specified, use container deploymentType = deploymentTypeContainer @@ -110,8 +110,8 @@ func deploySystemAgent(namespace string, ctrl *rsc.RemoteController, systemAgent // 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) + if deployAgentConfig.DeploymentType == nil { + deployAgentConfig.DeploymentType = iutil.MakeStrPtr(deploymentType) } } else { // Use defaults with configurable ports (router mode always interior) @@ -140,27 +140,27 @@ func deploySystemAgent(namespace string, ctrl *rsc.RemoteController, systemAgent } // Ensure router mode is always "interior" for system agents - if deployAgentConfig.RouterConfig.RouterMode == nil { + if deployAgentConfig.RouterMode == nil { interior := iofog.RouterModeInterior - deployAgentConfig.RouterConfig.RouterMode = &interior - } else if *deployAgentConfig.RouterConfig.RouterMode != iofog.RouterModeInterior { + deployAgentConfig.RouterMode = &interior + } else if *deployAgentConfig.RouterMode != iofog.RouterModeInterior { // Force to interior mode interior := iofog.RouterModeInterior - deployAgentConfig.RouterConfig.RouterMode = &interior + deployAgentConfig.RouterMode = &interior } - if deployAgentConfig.RouterConfig.EdgeRouterPort == nil { + if deployAgentConfig.EdgeRouterPort == nil { edgeRouterPort := 45671 - deployAgentConfig.RouterConfig.EdgeRouterPort = &edgeRouterPort + deployAgentConfig.EdgeRouterPort = &edgeRouterPort } - if deployAgentConfig.RouterConfig.InterRouterPort == nil { + if deployAgentConfig.InterRouterPort == nil { interRouterPort := 55671 - deployAgentConfig.RouterConfig.InterRouterPort = &interRouterPort + deployAgentConfig.InterRouterPort = &interRouterPort } - if deployAgentConfig.RouterConfig.MessagingPort == nil { + if deployAgentConfig.MessagingPort == nil { messagingPort := 5671 - deployAgentConfig.RouterConfig.MessagingPort = &messagingPort + deployAgentConfig.MessagingPort = &messagingPort } // System agents run NATS in server mode (like router interior). Apply default natsConfig when not provided. @@ -202,9 +202,9 @@ func deployNextSystemAgent(namespace string, ctrl *rsc.RemoteController, systemA 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 { + if systemAgentConfig != nil && systemAgentConfig.AgentConfiguration != nil && systemAgentConfig.AgentConfiguration.DeploymentType != nil { // Use DeploymentType from provided configuration - deploymentType = *systemAgentConfig.AgentConfiguration.AgentConfiguration.DeploymentType + deploymentType = *systemAgentConfig.AgentConfiguration.DeploymentType } else if systemAgentConfig != nil && systemAgentConfig.Package.Container.Image != "" { // If container image is specified, use container deploymentType = deploymentTypeContainer @@ -225,8 +225,8 @@ func deployNextSystemAgent(namespace string, ctrl *rsc.RemoteController, systemA // 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) + if deployAgentConfig.DeploymentType == nil { + deployAgentConfig.DeploymentType = iutil.MakeStrPtr(deploymentType) } // Override upstream routers for non-first controllers if deployAgentConfig.UpstreamRouters == nil { @@ -287,26 +287,26 @@ func deployNextSystemAgent(namespace string, ctrl *rsc.RemoteController, systemA } // Ensure router mode is always "interior" for system agents - if deployAgentConfig.RouterConfig.RouterMode == nil { + if deployAgentConfig.RouterMode == nil { interior := iofog.RouterModeInterior - deployAgentConfig.RouterConfig.RouterMode = &interior - } else if *deployAgentConfig.RouterConfig.RouterMode != iofog.RouterModeInterior { + deployAgentConfig.RouterMode = &interior + } else if *deployAgentConfig.RouterMode != iofog.RouterModeInterior { // Force to interior mode interior := iofog.RouterModeInterior - deployAgentConfig.RouterConfig.RouterMode = &interior + deployAgentConfig.RouterMode = &interior } - if deployAgentConfig.RouterConfig.EdgeRouterPort == nil { + if deployAgentConfig.EdgeRouterPort == nil { edgeRouterPort := 45671 - deployAgentConfig.RouterConfig.EdgeRouterPort = &edgeRouterPort + deployAgentConfig.EdgeRouterPort = &edgeRouterPort } - if deployAgentConfig.RouterConfig.InterRouterPort == nil { + if deployAgentConfig.InterRouterPort == nil { interRouterPort := 55671 - deployAgentConfig.RouterConfig.InterRouterPort = &interRouterPort + deployAgentConfig.InterRouterPort = &interRouterPort } - if deployAgentConfig.RouterConfig.MessagingPort == nil { + if deployAgentConfig.MessagingPort == nil { messagingPort := 5671 - deployAgentConfig.RouterConfig.MessagingPort = &messagingPort + deployAgentConfig.MessagingPort = &messagingPort } // System agents run NATS in server mode (like router interior). Apply default natsConfig when not provided. @@ -360,7 +360,7 @@ func prepareViewerURL(endpoint string) (string, error) { if err != nil || URL.Host == "" { URL, err = url.Parse("//" + endpoint) if err != nil { - return "", fmt.Errorf("failed to parse endpoint: %v", err) + return "", fmt.Errorf("failed to parse endpoint: %w", err) } } @@ -372,7 +372,7 @@ func prepareViewerURL(endpoint string) (string, error) { 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) + return "", fmt.Errorf("failed to split host and port: %w", err) } } else { host = URL.Host @@ -405,12 +405,12 @@ func updateViewerClientRootURL(controlPlane *rsc.RemoteControlPlane, endpoint st // Prepare viewer URL viewerURL, err := prepareViewerURL(endpoint) if err != nil { - return fmt.Errorf("failed to prepare viewer URL: %v", err) + return fmt.Errorf("failed to prepare viewer URL: %w", 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 fmt.Errorf("failed to update viewer client root URL: %w", err) } return nil @@ -435,7 +435,7 @@ func tagControllerImage(ctrl *rsc.RemoteController, image string) (err error) { 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"), + "sudo service iofog-agent restart", } // Execute commands @@ -479,11 +479,11 @@ func (exe remoteControlPlaneExecutor) postDeploy() (err error) { // 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) + return fmt.Errorf("failed to deploy system agent for first controller: %w", 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) + return fmt.Errorf("failed to deploy next-system agent for controller %d: %w", idx, err) } } var image string @@ -499,7 +499,7 @@ func (exe remoteControlPlaneExecutor) postDeploy() (err error) { } // 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) + return fmt.Errorf("failed to tag controller image for controller %d: %w", idx, err) } } return nil @@ -635,7 +635,7 @@ func validateMultiControllerHTTPS(controlPlane *rsc.RemoteControlPlane) error { // 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) + return fmt.Errorf("controller %d (%s): %w", idx, controller.Name, err) } } } @@ -654,7 +654,7 @@ func validateMultiControllerRouterCA(controlPlane *rsc.RemoteControlPlane) error 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) + return fmt.Errorf("first controller (%s): %w", firstController.Name, err) } // Check that other controllers don't have CA config @@ -781,7 +781,7 @@ func (exe remoteControlPlaneExecutor) transferControllerImages() error { if err != nil { return fmt.Errorf("controller %s: %w", controller.Name, err) } - engine, err := deployairgap.ResolveContainerEngine(controller.SystemAgent.AgentConfiguration.AgentConfiguration.ContainerEngine) + engine, err := deployairgap.ResolveContainerEngine(controller.SystemAgent.AgentConfiguration.ContainerEngine) if err != nil { return fmt.Errorf("controller %s: %w", controller.Name, err) } @@ -829,7 +829,7 @@ func (exe remoteControlPlaneExecutor) transferSystemAgentImages() error { return fmt.Errorf("system agent for controller %s: %w", controller.Name, err) } - engine, err := deployairgap.ResolveContainerEngine(controller.SystemAgent.AgentConfiguration.AgentConfiguration.ContainerEngine) + engine, err := deployairgap.ResolveContainerEngine(controller.SystemAgent.AgentConfiguration.ContainerEngine) if err != nil { return fmt.Errorf("system agent for controller %s: %w", controller.Name, err) } diff --git a/internal/deploy/execute.go b/internal/deploy/execute.go index b6d83ba37..b48287593 100644 --- a/internal/deploy/execute.go +++ b/internal/deploy/execute.go @@ -218,8 +218,8 @@ func Execute(opt *Options) (err error) { // 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 + if deployConfig != nil && deployConfig.Host != nil && *deployConfig.Host != "" { + apiHost = *deployConfig.Host } for _, configGenericExecutor := range executorsMap[config.AgentConfigKind] { 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..4c3a0241a 100644 --- a/internal/deploy/offlineimage/executor.go +++ b/internal/deploy/offlineimage/executor.go @@ -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) } diff --git a/internal/deploy/offlineimage/images.go b/internal/deploy/offlineimage/images.go index 48a11e82a..f347c131f 100644 --- a/internal/deploy/offlineimage/images.go +++ b/internal/deploy/offlineimage/images.go @@ -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) diff --git a/internal/deploy/volume/local.go b/internal/deploy/volume/local.go index 504e2cb85..451f453ee 100644 --- a/internal/deploy/volume/local.go +++ b/internal/deploy/volume/local.go @@ -23,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/describe/application.go b/internal/describe/application.go index d4e8181e8..8d713ee87 100644 --- a/internal/describe/application.go +++ b/internal/describe/application.go @@ -1,6 +1,8 @@ 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" @@ -36,7 +38,8 @@ 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 diff --git a/internal/describe/config_map.go b/internal/describe/config_map.go index 9da4a8725..d6f67be04 100644 --- a/internal/describe/config_map.go +++ b/internal/describe/config_map.go @@ -65,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 } @@ -73,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/utils.go b/internal/describe/utils.go index 0498414cd..a3bf555e9 100644 --- a/internal/describe/utils.go +++ b/internal/describe/utils.go @@ -1,6 +1,7 @@ package describe import ( + "errors" "fmt" "time" @@ -21,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 @@ -150,9 +152,10 @@ func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName Registry: client.RegistryTypeIDRegistryTypeDict[registryID], } for _, img := range imgArray { - if img.AgentTypeID == 1 { + switch img.AgentTypeID { + case 1: images.X86 = img.ContainerImage - } else if img.AgentTypeID == 2 { + case 2: images.ARM = img.ContainerImage } } diff --git a/internal/detach/exec/agent/execute.go b/internal/detach/exec/agent/execute.go index bf4c5281f..1b0e4c5b8 100644 --- a/internal/detach/exec/agent/execute.go +++ b/internal/detach/exec/agent/execute.go @@ -41,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{ @@ -50,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/exec/agent.go b/internal/exec/agent.go index 64f527b15..d8f19b80a 100644 --- a/internal/exec/agent.go +++ b/internal/exec/agent.go @@ -41,8 +41,7 @@ func (exe *agentExecutor) 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) } appName := fmt.Sprintf("system-%s", agent.Name) diff --git a/internal/exec/utils.go b/internal/exec/utils.go index 43f1f3eb2..2f9d511e6 100644 --- a/internal/exec/utils.go +++ b/internal/exec/utils.go @@ -115,21 +115,13 @@ func formatWebSocketError(err error) string { 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] - } + reason := strings.TrimSuffix(strings.TrimSpace(errStr[idx+7:]), ".") return reason } // 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] - } + reason := strings.TrimSuffix(strings.TrimSpace(errStr[idx+18:]), ".") return reason } @@ -161,10 +153,7 @@ func extractCloseReason(errStr string) string { // 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] - } + reason := strings.TrimSuffix(strings.TrimSpace(afterClose[colonIdx+1:]), ".") return reason } diff --git a/internal/execute/utils.go b/internal/execute/utils.go index ddac095b5..9ea07ca80 100644 --- a/internal/execute/utils.go +++ b/internal/execute/utils.go @@ -2,6 +2,7 @@ package execute import ( "bytes" + "errors" "fmt" "io" "os" @@ -130,7 +131,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 } diff --git a/internal/get/applications.go b/internal/get/applications.go index 3a7a9af80..def725d43 100644 --- a/internal/get/applications.go +++ b/internal/get/applications.go @@ -1,6 +1,7 @@ package get import ( + "errors" "fmt" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" @@ -49,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 } diff --git a/internal/logs/config.go b/internal/logs/config.go index 1234702b8..28b0a59f1 100644 --- a/internal/logs/config.go +++ b/internal/logs/config.go @@ -79,7 +79,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/resource/controlplane_test.go b/internal/resource/controlplane_test.go index 3dc4525e6..b444245f3 100644 --- a/internal/resource/controlplane_test.go +++ b/internal/resource/controlplane_test.go @@ -1,8 +1,9 @@ package resource import ( - "github.com/eclipse-iofog/iofogctl/pkg/util" "testing" + + "github.com/eclipse-iofog/iofogctl/pkg/util" ) const ( @@ -168,7 +169,7 @@ func TestLocalControlPlane(t *testing.T) { }); err != nil { t.Error(err) } - cp.Sanitize() + _ = cp.Sanitize() if endpoint, err := cp.GetEndpoint(); err != nil || !util.IsLocalHost(endpoint) { t.Errorf("Wrong endpoint: %s", endpoint) 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/types.go b/internal/resource/types.go index 24495de06..84c3aa307 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -478,6 +478,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/util/client/client.go b/internal/util/client/client.go index 34d3c02b6..98020d645 100644 --- a/internal/util/client/client.go +++ b/internal/util/client/client.go @@ -202,14 +202,14 @@ func newControllerClient(namespace string) (*client.Client, error) { 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) // Use SessionLogin to attempt to refresh the session util.SpinHandlePrompt() refreshedClient, err := client.SessionLogin(client.Options{BaseURL: baseURL}, 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 @@ -248,11 +248,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 @@ -277,7 +277,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 @@ -375,21 +375,21 @@ func refreshClientAuthentication(namespace string) (*client.Client, error) { refreshedClient, err := client.SessionLogin(client.Options{BaseURL: baseURL}, 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 @@ -417,7 +417,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/terminal/terminal.go b/internal/util/terminal/terminal.go index e4e78a8c6..62e880988 100644 --- a/internal/util/terminal/terminal.go +++ b/internal/util/terminal/terminal.go @@ -137,7 +137,7 @@ func (t *Terminal) handleInput(data []byte) bool { // Send everything as stdin to remote terminal msg := ws.NewMessage(ws.MessageTypeStdin, data, t.wsClient.GetMicroserviceUUID(), t.wsClient.GetExecID()) - t.wsClient.SendMessage(msg) + _ = t.wsClient.SendMessage(msg) return false } @@ -161,7 +161,7 @@ func (t *Terminal) redrawInputLine() { 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 + fmt.Fprintf(os.Stdout, "\x1b[%dC", t.cursorPos) // Move to cursor position } os.Stdout.Sync() } @@ -231,7 +231,7 @@ func (t *Terminal) cleanup() { t.wsClient.Close() } if t.oldState != nil { - term.Restore(int(os.Stdin.Fd()), t.oldState) + _ = term.Restore(int(os.Stdin.Fd()), t.oldState) t.oldState = nil } }) @@ -241,7 +241,7 @@ 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) + return fmt.Errorf("failed to set terminal to raw mode: %w", err) } defer t.cleanup() diff --git a/internal/util/update_openid_client.go b/internal/util/update_openid_client.go index b943bae6a..95d2cdbcf 100644 --- a/internal/util/update_openid_client.go +++ b/internal/util/update_openid_client.go @@ -50,19 +50,19 @@ func UpdateECNViewerClientRootURL(auth resource.Auth, newRootURL string) error { ctx := context.Background() token, err := config.Token(ctx) if err != nil { - return fmt.Errorf("failed to obtain access token: %v", err) + return fmt.Errorf("failed to obtain access token: %w", 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) + return fmt.Errorf("failed to get client ID: %w", 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 fmt.Errorf("failed to update client root URL: %w", err) } return nil @@ -76,7 +76,7 @@ func getKeycloakClientID(auth resource.Auth, clientID, adminToken string) (strin // Create request req, err := http.NewRequest("GET", adminURL, nil) if err != nil { - return "", fmt.Errorf("failed to create get client request: %v", err) + return "", fmt.Errorf("failed to create get client request: %w", err) } req.Header.Set("Authorization", "Bearer "+adminToken) @@ -86,7 +86,7 @@ func getKeycloakClientID(auth resource.Auth, clientID, adminToken string) (strin 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) + return "", fmt.Errorf("failed to execute get client request: %w", err) } defer resp.Body.Close() @@ -99,7 +99,7 @@ func getKeycloakClientID(auth resource.Auth, clientID, adminToken string) (strin // 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) + return "", fmt.Errorf("failed to decode clients response: %w", err) } // Find the client with matching clientID @@ -127,13 +127,13 @@ func updateClientRootURL(auth resource.Auth, clientID, newRootURL, adminToken st // Marshal to JSON payloadJSON, err := json.Marshal(updatePayload) if err != nil { - return fmt.Errorf("failed to marshal update payload: %v", err) + return fmt.Errorf("failed to marshal update payload: %w", 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) + return fmt.Errorf("failed to create update client request: %w", err) } req.Header.Set("Authorization", "Bearer "+adminToken) @@ -144,7 +144,7 @@ func updateClientRootURL(auth resource.Auth, clientID, newRootURL, adminToken st 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) + return fmt.Errorf("failed to execute update client request: %w", err) } defer resp.Body.Close() diff --git a/internal/util/websocket/client.go b/internal/util/websocket/client.go index 861d99fa0..115fe7cef 100644 --- a/internal/util/websocket/client.go +++ b/internal/util/websocket/client.go @@ -2,6 +2,7 @@ package websocket import ( "crypto/tls" + "errors" "fmt" "net/http" "strings" @@ -51,7 +52,8 @@ func (c *Client) Connect(url string, headers http.Header) error { 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 { + closeErr := &websocket.CloseError{} + if errors.As(err, &closeErr) { c.errMutex.Lock() c.err = util.NewError(fmt.Sprintf("connection closed by server (code: %d, reason: %s)", closeErr.Code, closeErr.Text)) c.errMutex.Unlock() @@ -104,7 +106,7 @@ func (c *Client) setupPingPong() { }) // Set initial read deadline - c.conn.SetReadDeadline(time.Now().Add(DefaultPingInterval * 2 * time.Millisecond)) + _ = c.conn.SetReadDeadline(time.Now().Add(DefaultPingInterval * 2 * time.Millisecond)) } // startPingLoop starts the periodic ping sending loop @@ -210,7 +212,8 @@ func (c *Client) ReadMessage() (*Message, error) { // 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 { + closeErr := &websocket.CloseError{} + if errors.As(err, &closeErr) { err = util.NewError(fmt.Sprintf("connection closed by server (code: %d, reason: %s)", closeErr.Code, closeErr.Text)) } } else { @@ -247,7 +250,7 @@ func (c *Client) ReadMessage() (*Message, error) { c.pongMutex.Unlock() // Extend read deadline - c.conn.SetReadDeadline(time.Now().Add(DefaultPingInterval * 2 * time.Millisecond)) + _ = c.conn.SetReadDeadline(time.Now().Add(DefaultPingInterval * 2 * time.Millisecond)) return msg, nil } @@ -317,7 +320,8 @@ func (c *Client) IsNormalClosure(err error) bool { } // Check for "use of closed network connection" during intentional close - if closeErr, ok := err.(*websocket.CloseError); ok { + closeErr := &websocket.CloseError{} + if errors.As(err, &closeErr) { return closeErr.Code == websocket.CloseNormalClosure || closeErr.Code == websocket.CloseGoingAway || closeErr.Code == websocket.CloseNoStatusReceived diff --git a/internal/util/websocket/message.go b/internal/util/websocket/message.go index 8ec9b1ef9..f09a7d134 100644 --- a/internal/util/websocket/message.go +++ b/internal/util/websocket/message.go @@ -41,7 +41,7 @@ func (m *Message) Encode() ([]byte, error) { 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 nil, fmt.Errorf("failed to decode MessagePack data: %w", err) } return &msg, nil } diff --git a/pkg/iofog/constants.go b/pkg/iofog/constants.go index eecf038d5..024bca98d 100644 --- a/pkg/iofog/constants.go +++ b/pkg/iofog/constants.go @@ -2,7 +2,7 @@ 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/controller.go b/pkg/iofog/install/controller.go index 8e3f9f9fb..cc74eaa54 100644 --- a/pkg/iofog/install/controller.go +++ b/pkg/iofog/install/controller.go @@ -326,7 +326,7 @@ func (ctrl *Controller) CopyScript(srcDir, filename, destDir string) (err error) return err } - // Copy to /tmp for backwards compatability + // Copy to /tmp for backwards compatibility reader := strings.NewReader(staticFile) if err := ctrl.ssh.CopyTo(reader, destDir, filename, "0775", int64(len(staticFile))); err != nil { return err diff --git a/pkg/iofog/install/k8s.go b/pkg/iofog/install/k8s.go index 753079b24..55708e5f8 100644 --- a/pkg/iofog/install/k8s.go +++ b/pkg/iofog/install/k8s.go @@ -799,10 +799,8 @@ func (k8s *Kubernetes) GetControllerEndpoint() (endpoint string, err error) { if err != nil { return "", err } - isViewerDns := false - if k8s.isViewerDns != nil && *k8s.isViewerDns { - isViewerDns = true - } + isViewerDns := k8s.isViewerDns != nil && *k8s.isViewerDns + formattedURL, err := k8s.formatEndpoint(ip, port, isViewerDns) if err != nil { return "", err @@ -810,10 +808,7 @@ func (k8s *Kubernetes) GetControllerEndpoint() (endpoint string, err error) { 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) } diff --git a/pkg/iofog/install/local_container.go b/pkg/iofog/install/local_container.go index 67538085d..2d05ff85a 100644 --- a/pkg/iofog/install/local_container.go +++ b/pkg/iofog/install/local_container.go @@ -151,7 +151,7 @@ type LocalSystemImages struct { NatsEnabled *bool // nil = default enabled } -// NewLocalControllerConfig generats a static controller config +// NewLocalControllerConfig generates a static controller config func NewLocalControllerConfig(image string, credentials Credentials, auth Auth, db Database, events Events, systemImages *LocalSystemImages) *LocalContainerConfig { if image == "" { image = util.GetControllerImage() @@ -315,7 +315,7 @@ func (lc *LocalContainer) CleanContainer(name string) error { 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 { + if err := lc.client.ContainerStop(ctx, container.ID, dockerContainer.StopOptions{Signal: "SIGTERM", Timeout: nil}); err != nil { return err } @@ -327,7 +327,7 @@ 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 { + if err := lc.client.ContainerStop(ctx, id, dockerContainer.StopOptions{Signal: "SIGTERM", Timeout: nil}); err != nil { return err } diff --git a/pkg/iofog/install/remote_agent.go b/pkg/iofog/install/remote_agent.go index 85e60e87a..ca007cb83 100644 --- a/pkg/iofog/install/remote_agent.go +++ b/pkg/iofog/install/remote_agent.go @@ -273,7 +273,7 @@ func (agent *RemoteAgent) Bootstrap() error { }, { cmd: agent.procs.Deps.getCommand(), - msg: "Installing dependancies on Agent " + agent.name, + msg: "Installing dependencies on Agent " + agent.name, }, { cmd: fmt.Sprintf("sudo %s", agent.procs.Install.getCommand()), diff --git a/pkg/util/errors.go b/pkg/util/errors.go index 520554bec..c9e850bfc 100644 --- a/pkg/util/errors.go +++ b/pkg/util/errors.go @@ -107,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/print.go b/pkg/util/print.go index b875438f9..6834d5f00 100644 --- a/pkg/util/print.go +++ b/pkg/util/print.go @@ -19,7 +19,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() } @@ -29,7 +29,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() } @@ -56,12 +56,12 @@ 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") } 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) } From 2d2d97f1008ea239ef3363dccd6e4a04e9153002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 08:01:04 +0300 Subject: [PATCH 08/63] Bump operator and SDK to v3.8 and add CLI_CP_CR_NAME ldflag. Pin iofog-operator v3.8.0-rc.1 and iofog-go-sdk v3.8.0-rc.3; inject flavor-specific ControlPlane CR name via Makefile and pkg/util. --- Makefile | 7 +++++-- go.mod | 24 ++++++++++++++---------- go.sum | 36 ++++++++++++++++++------------------ pkg/util/version.go | 7 +++---- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/Makefile b/Makefile index 38acdbd70..803c2dc04 100644 --- a/Makefile +++ b/Makefile @@ -29,17 +29,19 @@ 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 = downloads.datasance.com + PACKAGE_REPO_BASE = https://downloads.datasance.com OCI_SOURCE_REPO = https://github.com/Datasance/potctl 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://packagecloud.io/iofog + PACKAGE_REPO_BASE = https://iofog.datasance.com OCI_SOURCE_REPO = https://github.com/eclipse-iofog/iofogctl endif @@ -47,6 +49,7 @@ LDFLAGS += -X $(PREFIX).versionNumber=$(VERSION) -X $(PREFIX).commit=$(COMMIT) - 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) diff --git a/go.mod b/go.mod index 7e353909f..962e5aee4 100644 --- a/go.mod +++ b/go.mod @@ -7,26 +7,27 @@ require ( 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/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.3 + github.com/eclipse-iofog/iofog-operator/v3 v3.8.0-rc.1 github.com/gorilla/websocket v1.5.3 github.com/mitchellh/go-homedir v1.1.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/stretchr/testify v1.10.0 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 + golang.org/x/crypto v0.51.0 + golang.org/x/sys v0.45.0 + golang.org/x/term v0.43.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 ( @@ -109,6 +110,7 @@ require ( github.com/opencontainers/selinux v1.11.0 // indirect github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/proglottis/gpgme v0.1.3 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -139,9 +141,10 @@ require ( 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/net v0.55.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/text v0.37.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 @@ -154,9 +157,10 @@ 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 ) +replace github.com/eclipse-iofog/iofog-go-sdk/v3 => github.com/Datasance/iofog-go-sdk/v3 v3.8.0-rc.3 + exclude github.com/Sirupsen/logrus v1.4.2 exclude github.com/Sirupsen/logrus v1.4.1 diff --git a/go.sum b/go.sum index 9fbc44642..9932cbd51 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg6 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/Datasance/iofog-go-sdk/v3 v3.8.0-rc.3 h1:Ye2ZhAmXAYK6xiKmPXB23JA/GifRxJAPzw7r/Zin06Q= +github.com/Datasance/iofog-go-sdk/v3 v3.8.0-rc.3/go.mod h1:MU+YPxFRGFzBMboi1khtEGAoRwaF4yJX3KuBwFcfCKg= 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= @@ -71,10 +73,8 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= 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-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= @@ -368,8 +368,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 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/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= 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= @@ -387,8 +387,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL 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/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= 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= @@ -397,8 +397,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ 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/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -407,14 +407,14 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc 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.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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= 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/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= 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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -425,8 +425,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn 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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= 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= diff --git a/pkg/util/version.go b/pkg/util/version.go index 618b579bd..003e0de24 100644 --- a/pkg/util/version.go +++ b/pkg/util/version.go @@ -12,9 +12,10 @@ var ( cliBinaryName = "iofogctl" cliCrdGroup = "iofog.org" cliApiVersion = "iofog.org/v3" + cliCpCrName = "iofog" imageRegistry = "ghcr.io/eclipse-iofog" cliDocsUrl = "https://iofog.org" - packageRepoBase = "https://packagecloud.io/iofog" + packageRepoBase = "https://iofog.datasance.com" ociSourceRepo = "https://github.com/eclipse-iofog/iofogctl" controllerTag = "undefined" @@ -56,6 +57,7 @@ func GetVersion() Version { 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 } @@ -76,9 +78,6 @@ func GetOperatorImage() string { func GetRouterImage() string { return fmt.Sprintf("%s/%s:%s", imageRegistry, routerImage, routerTag) } -func GetRouterARMImage() string { - return fmt.Sprintf("%s/%s:%s", imageRegistry, routerARMImage, routerTag) -} func GetNatsImage() string { return fmt.Sprintf("%s/%s:%s", imageRegistry, natsImage, natsTag) } From 856e1f10cd4e436ab68c8c73c005bec2df8d70cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 08:01:09 +0300 Subject: [PATCH 09/63] Align KubernetesControlPlane structs with v3.8 reference YAML. Add embedded/external auth, controller URLs, NATS spec, and cpv3 types; retire Keycloak and ecnViewer fields from K8s paths; add golden fixtures. --- internal/resource/controlplane.go | 1 + internal/resource/controlplane_test.go | 50 ++++++ internal/resource/cpv3.go | 77 ++++++++++ internal/resource/k8s_controlplane.go | 8 +- .../resource/k8s_controlplane_golden_test.go | 145 ++++++++++++++++++ internal/resource/local_controlplane.go | 6 + internal/resource/remote_controlplane.go | 6 + .../testdata/k8s/controlplane-datasance.yaml | 68 ++++++++ .../testdata/k8s/controlplane-iofog.yaml | 68 ++++++++ .../resource/testdata/k8s/controlplane.yaml | 90 +++++++++++ internal/resource/trust.go | 15 ++ internal/resource/types.go | 92 ++++++++--- 12 files changed, 601 insertions(+), 25 deletions(-) create mode 100644 internal/resource/cpv3.go create mode 100644 internal/resource/k8s_controlplane_golden_test.go create mode 100644 internal/resource/testdata/k8s/controlplane-datasance.yaml create mode 100644 internal/resource/testdata/k8s/controlplane-iofog.yaml create mode 100644 internal/resource/testdata/k8s/controlplane.yaml create mode 100644 internal/resource/trust.go diff --git a/internal/resource/controlplane.go b/internal/resource/controlplane.go index 94aec6fa7..039f0eb59 100644 --- a/internal/resource/controlplane.go +++ b/internal/resource/controlplane.go @@ -6,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 b444245f3..fbe588c0e 100644 --- a/internal/resource/controlplane_test.go +++ b/internal/resource/controlplane_test.go @@ -1,9 +1,11 @@ package resource import ( + "strings" "testing" "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" ) const ( @@ -11,11 +13,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") 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/k8s_controlplane.go b/internal/resource/k8s_controlplane.go index 0c21d7933..ce9ad6eb2 100644 --- a/internal/resource/k8s_controlplane.go +++ b/internal/resource/k8s_controlplane.go @@ -8,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"` @@ -17,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 } @@ -131,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_controlplane.go b/internal/resource/local_controlplane.go index e693ed220..1493e1dcd 100644 --- a/internal/resource/local_controlplane.go +++ b/internal/resource/local_controlplane.go @@ -5,6 +5,7 @@ import ( ) type LocalControlPlane struct { + CA string `yaml:"ca,omitempty"` IofogUser IofogUser `yaml:"iofogUser"` Controller *LocalController `yaml:"controller,omitempty"` Database Database `yaml:"database"` @@ -14,6 +15,10 @@ type LocalControlPlane struct { Nats *NatsEnabledConfig `yaml:"nats,omitempty"` } +func (cp *LocalControlPlane) GetTrustCA() string { + return cp.CA +} + func (cp *LocalControlPlane) GetUser() IofogUser { return cp.IofogUser } @@ -85,6 +90,7 @@ func (cp *LocalControlPlane) Clone() ControlPlane { } } return &LocalControlPlane{ + CA: cp.CA, IofogUser: cp.IofogUser, Controller: cp.Controller.Clone().(*LocalController), Database: cp.Database, diff --git a/internal/resource/remote_controlplane.go b/internal/resource/remote_controlplane.go index 4d58252b8..03814be02 100644 --- a/internal/resource/remote_controlplane.go +++ b/internal/resource/remote_controlplane.go @@ -8,6 +8,7 @@ import ( type RemoteSystemMicroservices = install.RemoteSystemMicroservices type RemoteControlPlane struct { + CA string `yaml:"ca,omitempty"` IofogUser IofogUser `yaml:"iofogUser"` Controllers []RemoteController `yaml:"controllers"` Database Database `yaml:"database"` @@ -21,6 +22,10 @@ type RemoteControlPlane struct { Airgap bool `yaml:"airgap,omitempty"` } +func (cp *RemoteControlPlane) GetTrustCA() string { + return cp.CA +} + func (cp *RemoteControlPlane) GetUser() IofogUser { return cp.IofogUser } @@ -119,6 +124,7 @@ func (cp *RemoteControlPlane) Clone() ControlPlane { controllers := make([]RemoteController, len(cp.Controllers)) copy(controllers, cp.Controllers) return &RemoteControlPlane{ + CA: cp.CA, IofogUser: cp.IofogUser, Database: cp.Database, Auth: cp.Auth, diff --git a/internal/resource/testdata/k8s/controlplane-datasance.yaml b/internal/resource/testdata/k8s/controlplane-datasance.yaml new file mode 100644 index 000000000..fc53dc2c9 --- /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.1 + router: ghcr.io/datasance/router:3.8.0-rc.1 + nats: ghcr.io/datasance/nats:2.14.2-rc.1 +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..a06d45a66 --- /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.1 + router: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + nats: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 +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..cd23b041f --- /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.1 + router: ghcr.io/datasance/router:3.8.0-rc.1 + nats: ghcr.io/datasance/nats:2.14.2-rc.1 + 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/trust.go b/internal/resource/trust.go new file mode 100644 index 000000000..61c61ed64 --- /dev/null +++ b/internal/resource/trust.go @@ -0,0 +1,15 @@ +package resource + +// 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 "" +} diff --git a/internal/resource/types.go b/internal/resource/types.go index 84c3aa307..f30f226bd 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -69,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 { @@ -116,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 { @@ -135,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"` } @@ -191,27 +232,30 @@ 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{ +// ArchStringMap map human readable fog type to Controller fog type +var ArchStringMap = map[string]int64{ "auto": 0, "x86": 1, "arm": 2, } -// FogTypeIntMap map Controller fog type to human readable fog type -var FogTypeIntMap = map[int]string{ +// ArchIntMap map Controller fog type to human readable fog type +var ArchIntMap = map[int]string{ 0: "auto", 1: "x86", 2: "arm", } -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 { From bd581e601b458de31647d832bab390e583201b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 08:01:15 +0300 Subject: [PATCH 10/63] Add password complexity validation for embedded auth bootstrap and iofogUser. --- internal/validate/password.go | 37 ++++++++++++++++++++++++++++++ internal/validate/password_test.go | 25 ++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 internal/validate/password.go create mode 100644 internal/validate/password_test.go diff --git a/internal/validate/password.go b/internal/validate/password.go new file mode 100644 index 000000000..a77961dd9 --- /dev/null +++ b/internal/validate/password.go @@ -0,0 +1,37 @@ +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. +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")) + }) +} From 033216e15a9354a4471269dff259cb8a3e730c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 08:01:20 +0300 Subject: [PATCH 11/63] Deploy K8s ControlPlane via flavor CRD, operator RBAC, and CLI_CP_CR_NAME. Install ControlPlane CR only (no Application CRD); wait on IsReady(); resolve controller endpoint via LB and ingress name controller. --- pkg/iofog/install/controller.go | 53 +++- pkg/iofog/install/k8s.go | 398 +++++++++---------------- pkg/iofog/install/k8s_crd.go | 83 ++++++ pkg/iofog/install/k8s_microservices.go | 146 ++++----- pkg/iofog/install/k8s_operator_test.go | 74 +++++ pkg/iofog/install/k8s_scheme.go | 22 ++ pkg/iofog/install/k8s_util.go | 19 +- pkg/iofog/install/local_container.go | 8 +- pkg/iofog/install/remote_agent.go | 22 +- pkg/iofog/install/types.go | 32 +- 10 files changed, 446 insertions(+), 411 deletions(-) create mode 100644 pkg/iofog/install/k8s_crd.go create mode 100644 pkg/iofog/install/k8s_operator_test.go create mode 100644 pkg/iofog/install/k8s_scheme.go diff --git a/pkg/iofog/install/controller.go b/pkg/iofog/install/controller.go index cc74eaa54..853ebbbd3 100644 --- a/pkg/iofog/install/controller.go +++ b/pkg/iofog/install/controller.go @@ -19,8 +19,10 @@ const ( ) type RemoteSystemImages struct { - ARM string `yaml:"arm,omitempty"` - X86 string `yaml:"x86,omitempty"` + ARM string `yaml:"arm,omitempty"` + AMD64 string `yaml:"amd64,omitempty"` + ARM64 string `yaml:"arm64,omitempty"` + RISCV64 string `yaml:"riscv64,omitempty"` } type RemoteSystemMicroservices struct { @@ -469,11 +471,17 @@ func appendControllerBaseEnv(env []string, ctrl *Controller) []string { 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.AMD64 != "" { + env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_1=%s\"", ctrl.SystemMicroservices.Router.AMD64)) + } + if ctrl.SystemMicroservices.Router.ARM64 != "" { + env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_2=%s\"", ctrl.SystemMicroservices.Router.ARM64)) + } + if ctrl.SystemMicroservices.Router.RISCV64 != "" { + env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_3=%s\"", ctrl.SystemMicroservices.Router.RISCV64)) } if ctrl.SystemMicroservices.Router.ARM != "" { - env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_2=%s\"", ctrl.SystemMicroservices.Router.ARM)) + env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_4=%s\"", ctrl.SystemMicroservices.Router.ARM)) } return env } @@ -512,23 +520,36 @@ func appendAuthEnv(env []string, ctrl *Controller) []string { } func appendNatsEnv(env []string, ctrl *Controller) []string { - natsX86 := ctrl.SystemMicroservices.Nats.X86 + natsAMD64 := ctrl.SystemMicroservices.Nats.AMD64 + natsARM64 := ctrl.SystemMicroservices.Nats.ARM64 + natsRISCV64 := ctrl.SystemMicroservices.Nats.RISCV64 natsARM := ctrl.SystemMicroservices.Nats.ARM - if natsX86 == "" && natsARM != "" { - natsX86 = natsARM + if natsAMD64 == "" && natsARM64 != "" { + natsAMD64 = natsARM64 + } + if natsARM64 == "" && natsAMD64 != "" { + natsARM64 = natsAMD64 + } + if natsRISCV64 == "" && natsARM != "" { + natsRISCV64 = natsARM + } + if natsARM == "" && natsRISCV64 != "" { + natsARM = natsRISCV64 + } + if natsAMD64 == "" && natsARM64 == "" && natsRISCV64 == "" { + natsAMD64 = util.GetNatsImage() } - if natsARM == "" && natsX86 != "" { - natsARM = natsX86 + if natsAMD64 != "" { + env = append(env, fmt.Sprintf("\"NATS_IMAGE_1=%s\"", natsAMD64)) } - if natsX86 == "" && natsARM == "" { - natsX86 = util.GetNatsImage() - natsARM = natsX86 + if natsARM64 != "" { + env = append(env, fmt.Sprintf("\"NATS_IMAGE_2=%s\"", natsARM64)) } - if natsX86 != "" { - env = append(env, fmt.Sprintf("\"NATS_IMAGE_1=%s\"", natsX86)) + if natsRISCV64 != "" { + env = append(env, fmt.Sprintf("\"NATS_IMAGE_3=%s\"", natsRISCV64)) } if natsARM != "" { - env = append(env, fmt.Sprintf("\"NATS_IMAGE_2=%s\"", natsARM)) + env = append(env, fmt.Sprintf("\"NATS_IMAGE_4=%s\"", natsARM)) } natsEnabled := true if ctrl.NatsEnabled != nil { diff --git a/pkg/iofog/install/k8s.go b/pkg/iofog/install/k8s.go index 55708e5f8..4ad81d4d8 100644 --- a/pkg/iofog/install/k8s.go +++ b/pkg/iofog/install/k8s.go @@ -1,22 +1,18 @@ 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" @@ -28,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 @@ -44,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 @@ -119,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) { @@ -141,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 { @@ -170,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 @@ -178,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{ @@ -201,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 @@ -215,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 { @@ -264,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... } } @@ -459,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 @@ -468,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, }, } @@ -483,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 @@ -496,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: + } + } +} + +func (k8s *Kubernetes) ingressUsesHTTPS() bool { + if k8s.httpsEnabled != nil && *k8s.httpsEnabled { + return true + } + if k8s.cpSpec.Ingresses.Controller.SecretName != "" { + return true } + return false +} - err = util.NewError("Did not receive any valid Ingress events") - return +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) { @@ -659,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 @@ -769,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 @@ -783,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 @@ -797,19 +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 := k8s.isViewerDns != nil && *k8s.isViewerDns + 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 := k8s.httpsEnabled != nil && *k8s.httpsEnabled - return util.GetControllerEndpoint(endpoint, useHTTPS) } 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 b064d34fa..bf3bd35dd 100644 --- a/pkg/iofog/install/k8s_microservices.go +++ b/pkg/iofog/install/k8s_microservices.go @@ -34,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", @@ -169,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 6c415fb54..da888af4b 100644 --- a/pkg/iofog/install/k8s_util.go +++ b/pkg/iofog/install/k8s_util.go @@ -6,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 { @@ -30,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_container.go b/pkg/iofog/install/local_container.go index 2d05ff85a..ac1f5a946 100644 --- a/pkg/iofog/install/local_container.go +++ b/pkg/iofog/install/local_container.go @@ -178,14 +178,8 @@ func NewLocalControllerConfig(image string, credentials Credentials, auth Auth, "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, } + _ = auth // v3.8 auth is embedded/external OIDC — not Keycloak env vars // Add Events environment variables only if Events is explicitly configured if events.AuditEnabled != nil { diff --git a/pkg/iofog/install/remote_agent.go b/pkg/iofog/install/remote_agent.go index ca007cb83..53b12da5b 100644 --- a/pkg/iofog/install/remote_agent.go +++ b/pkg/iofog/install/remote_agent.go @@ -329,18 +329,18 @@ func (agent *RemoteAgent) Configure(controllerEndpoint string, user IofogUser) ( } func (agent *RemoteAgent) SetInitialConfig( - name, fogType string, + name, arch string, // latitude, longitude float64, - // description, fogType string, + // description, arch 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 Arch (string) to required format if necessary + archValue := arch + if arch == "" { + archValue = "auto" // Default value if arch is empty } // Convert WatchdogEnabled (*bool) to "on"/"off" @@ -357,7 +357,7 @@ func (agent *RemoteAgent) SetInitialConfig( // Extract values from agentConfig and construct options configOptions := map[string]string{ - "-ft": fogTypeValue, + "-ft": archValue, // "-gps": gpsCoordinates, } @@ -365,8 +365,8 @@ func (agent *RemoteAgent) SetInitialConfig( if agentConfig.NetworkInterface != nil && *agentConfig.NetworkInterface != "" { configOptions["-n"] = *agentConfig.NetworkInterface } - if agentConfig.DockerURL != nil && *agentConfig.DockerURL != "" { - configOptions["-c"] = *agentConfig.DockerURL + if agentConfig.ContainerEngineURL != nil && *agentConfig.ContainerEngineURL != "" { + configOptions["-c"] = *agentConfig.ContainerEngineURL } if agentConfig.DiskLimit != nil { configOptions["-d"] = strconv.FormatInt(*agentConfig.DiskLimit, 10) @@ -404,8 +404,8 @@ func (agent *RemoteAgent) SetInitialConfig( 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.PruningFrequency != nil { + configOptions["-pf"] = strconv.FormatFloat(*agentConfig.PruningFrequency, 'f', -1, 64) } // if agentConfig.GpsDevice != nil && *agentConfig.GpsDevice != "" { // configOptions["-gpsd"] = *agentConfig.GpsDevice diff --git a/pkg/iofog/install/types.go b/pkg/iofog/install/types.go index ae9030d5d..5c2df0eb1 100644 --- a/pkg/iofog/install/types.go +++ b/pkg/iofog/install/types.go @@ -13,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 @@ -82,18 +74,12 @@ type Pod struct { } 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 } From 53cd9d3e22c8170501430cc2e5e0bfb51f0c250e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 08:01:25 +0300 Subject: [PATCH 12/63] Translate KubernetesControlPlane YAML to operator ControlPlane CR. Strip CLI-only fields and apply flavor CR name with golden tests for iofog.org and datasance.com groups. --- .../k8s/testdata/cp-cr-datasance.yaml | 66 ++++++++ .../k8s/testdata/cp-cr-iofog.yaml | 66 ++++++++ internal/deploy/controlplane/k8s/translate.go | 153 ++++++++++++++++++ .../deploy/controlplane/k8s/translate_test.go | 124 ++++++++++++++ 4 files changed, 409 insertions(+) create mode 100644 internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml create mode 100644 internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml create mode 100644 internal/deploy/controlplane/k8s/translate.go create mode 100644 internal/deploy/controlplane/k8s/translate_test.go 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..f3c7c9d72 --- /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.1 + router: ghcr.io/datasance/router:3.8.0-rc.1 + nats: ghcr.io/datasance/nats:2.14.2-rc.1 + 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..5574809b9 --- /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.1 + router: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 + nats: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + 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..81c503150 --- /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.1", + routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", + natsImage: "ghcr.io/datasance/nats:2.14.2-rc.1", + }) + 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.1", + routerImage: "ghcr.io/eclipse-iofog/router:3.8.0-rc.1", + natsImage: "ghcr.io/eclipse-iofog/nats:2.14.2-rc.1", + }) + 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.1", + routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", + natsImage: "ghcr.io/datasance/nats:2.14.2-rc.1", + }) + require.NotContains(t, got.Spec.Images.Controller, "operator") + require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.1", 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.1", + routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", + natsImage: "ghcr.io/datasance/nats:2.14.2-rc.1", + }) + require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.1", 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.1", 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) +} From 71d9d504635e11a457c5f1bd47446019c6f8e1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 08:01:30 +0300 Subject: [PATCH 13/63] Add namespace trust store and TLS probe for HTTPS controller endpoints. Persist spec.ca under ~/.iofog/v3/trust; probe system trust before API wait; add PrintWarning for untrusted or hostname-mismatch certificates. --- internal/config/config.go | 5 + internal/trust/probe.go | 64 ++++++++++++ internal/trust/probe_test.go | 91 +++++++++++++++++ internal/trust/store.go | 152 +++++++++++++++++++++++++++++ internal/trust/store_test.go | 90 +++++++++++++++++ internal/trust/tls.go | 136 ++++++++++++++++++++++++++ internal/trust/tls_connect_test.go | 67 +++++++++++++ internal/trust/wait.go | 55 +++++++++++ pkg/util/print.go | 11 +++ 9 files changed, 671 insertions(+) create mode 100644 internal/trust/probe.go create mode 100644 internal/trust/probe_test.go create mode 100644 internal/trust/store.go create mode 100644 internal/trust/store_test.go create mode 100644 internal/trust/tls.go create mode 100644 internal/trust/tls_connect_test.go create mode 100644 internal/trust/wait.go diff --git a/internal/config/config.go b/internal/config/config.go index af811085a..fc4c6dc6c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -217,6 +217,11 @@ func Flush() error { return flushNamespaces() } +// ConfigFolder returns the initialized CLI config root (e.g. ~/.iofog/v3). +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) 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..d7f7df6d1 --- /dev/null +++ b/internal/trust/store.go @@ -0,0 +1,152 @@ +package trust + +import ( + "encoding/base64" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/eclipse-iofog/iofogctl/internal/config" +) + +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, 0700); err != nil { + return err + } + path, err := caPath(namespace) + if err != nil { + return err + } + if err := os.WriteFile(path, pem, 0600); 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 := os.ReadFile(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 := os.ReadFile(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, 0700); err != nil { + return err + } + path, err := modePath(namespace) + if err != nil { + return err + } + return os.WriteFile(path, []byte(string(mode)), 0600) +} diff --git a/internal/trust/store_test.go b/internal/trust/store_test.go new file mode 100644 index 000000000..575877a41 --- /dev/null +++ b/internal/trust/store_test.go @@ -0,0 +1,90 @@ +package trust + +import ( + "encoding/base64" + "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 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..ef163e523 --- /dev/null +++ b/internal/trust/tls.go @@ -0,0 +1,136 @@ +package trust + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/url" + "os" + "strings" + + "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 := os.ReadFile(caFile) + if err != nil { + return TransportConfig{}, fmt.Errorf("read CA file: %w", err) + } + return TransportFromPEM(pem) +} + +// ResolveConnectTransport picks TLS settings for connect; caFile overrides the namespace 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(fmt.Sprintf( + `Controller TLS certificate is not trusted by the system CA store for namespace "%s". Certificate verification is disabled.`, + namespace, + )) + _ = 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: + return TransportConfig{ + SkipVerify: true, + TLSConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}, // #nosec G402 + } + } +} + +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" + } + } + if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") { + // bare IPv6 without brackets handled by Hostname() + } + 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..b1e2351df --- /dev/null +++ b/internal/trust/wait.go @@ -0,0 +1,55 @@ +package trust + +import ( + "context" + "fmt" + "io" + "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 { + _, _ = io.Copy(io.Discard, resp.Body) + resp.Body.Close() + 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/pkg/util/print.go b/pkg/util/print.go index 6834d5f00..228521595 100644 --- a/pkg/util/print.go +++ b/pkg/util/print.go @@ -12,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 @@ -65,3 +66,13 @@ func PrintError(message string) { message = FirstToUpper(message) 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() + } +} From 44c739059069d3c1fc2599dbd0543b658a22e3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 08:01:36 +0300 Subject: [PATCH 14/63] Wire embedded auth hook and interactive iofogUser password on K8s deploy. EnsureIofogUserEmbedded after controller API is ready; trust-aware bootstrap client; remove legacy Keycloak ecnViewer client updater. --- internal/auth/connect_login.go | 19 ++ internal/auth/controller_client.go | 162 ++++++++++++++++++ internal/auth/embedded_user.go | 87 ++++++++++ internal/auth/embedded_user_test.go | 161 +++++++++++++++++ internal/auth/prompt_password.go | 69 ++++++++ internal/auth/prompt_password_test.go | 84 +++++++++ internal/deploy/controlplane/k8s/execute.go | 140 +++++++++------ .../deploy/controlplane/k8s/execute_test.go | 133 ++++++++++++++ internal/util/update_openid_client.go | 152 +--------------- 9 files changed, 809 insertions(+), 198 deletions(-) create mode 100644 internal/auth/connect_login.go create mode 100644 internal/auth/controller_client.go create mode 100644 internal/auth/embedded_user.go create mode 100644 internal/auth/embedded_user_test.go create mode 100644 internal/auth/prompt_password.go create mode 100644 internal/auth/prompt_password_test.go create mode 100644 internal/deploy/controlplane/k8s/execute_test.go 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..cdc9fe917 --- /dev/null +++ b/internal/auth/controller_client.go @@ -0,0 +1,162 @@ +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) error { + _, err := c.doJSON(http.MethodPost, "/users", authHeaders(c.accessToken), req) + return err +} + +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..dab924fe7 --- /dev/null +++ b/internal/auth/embedded_user.go @@ -0,0 +1,87 @@ +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) 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 + } + if authUserExists(users, spec.User.Email) { + return nil + } + + return clt.createAuthUser(client.AuthUserCreateRequest{ + Email: spec.User.Email, + Password: password, + Groups: []string{"admin"}, + }) +} + +func authUserExists(users []client.AuthUserResponse, email string) bool { + for _, u := range users { + if u.Email == email { + return true + } + } + return false +} diff --git a/internal/auth/embedded_user_test.go b/internal/auth/embedded_user_test.go new file mode 100644 index 000000000..1ba7ffb50 --- /dev/null +++ b/internal/auth/embedded_user_test.go @@ -0,0 +1,161 @@ +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" +) + +type mockEmbeddedAuthClient struct { + loginEmail string + loginPassword string + loginErr error + users []client.AuthUserResponse + created []client.AuthUserCreateRequest + loginCalls int + listCalls int +} + +func (m *mockEmbeddedAuthClient) bootstrapLogin(email, password string) error { + m.loginCalls++ + m.loginEmail = email + m.loginPassword = 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) error { + m.created = append(m.created, 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.Zero(t, mock.loginCalls) +} + +func TestEnsureIofogUserEmbeddedCreatesWhenMissing(t *testing.T) { + t.Cleanup(resetEmbeddedAuthDeps) + mock := &mockEmbeddedAuthClient{users: []client.AuthUserResponse{}} + 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.Equal(t, 1, mock.loginCalls) + require.Equal(t, "admin", mock.loginEmail) + require.Equal(t, "BootstrapTest12!", mock.loginPassword) + require.Len(t, mock.created, 1) + require.Equal(t, "user@domain.com", mock.created[0].Email) + require.Equal(t, "LocalTest12!", mock.created[0].Password) + require.Equal(t, []string{"admin"}, mock.created[0].Groups) +} + +func TestEnsureIofogUserEmbeddedSkipsWhenExists(t *testing.T) { + t.Cleanup(resetEmbeddedAuthDeps) + mock := &mockEmbeddedAuthClient{ + users: []client.AuthUserResponse{{Email: "user@domain.com"}}, + } + 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.Equal(t, 1, mock.loginCalls) + require.Empty(t, mock.created) +} + +func TestEnsureIofogUserEmbeddedUsesInteractivePassword(t *testing.T) { + t.Cleanup(resetEmbeddedAuthDeps) + mock := &mockEmbeddedAuthClient{users: []client.AuthUserResponse{}} + newEmbeddedAuthClient = func(context.Context, string, string) (embeddedAuthClient, error) { + return mock, 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, "LocalTest12!", mock.created[0].Password) +} + +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, "") + } + 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/deploy/controlplane/k8s/execute.go b/internal/deploy/controlplane/k8s/execute.go index cbdd7a035..95cba771e 100644 --- a/internal/deploy/controlplane/k8s/execute.go +++ b/internal/deploy/controlplane/k8s/execute.go @@ -1,12 +1,16 @@ 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" ) @@ -77,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 { @@ -146,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 exe.controlPlane.Controller.ConsoleUrl == "" { + exe.controlPlane.Controller.ConsoleUrl = endpoint + } 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 @@ -203,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/util/update_openid_client.go b/internal/util/update_openid_client.go index 95d2cdbcf..ba3a29b46 100644 --- a/internal/util/update_openid_client.go +++ b/internal/util/update_openid_client.go @@ -1,158 +1,10 @@ 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: %w", 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: %w", 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: %w", 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: %w", 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: %w", 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: %w", 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: %w", err) - } - - // Create request - req, err := http.NewRequest("PUT", adminURL, bytes.NewBuffer(payloadJSON)) - if err != nil { - return fmt.Errorf("failed to create update client request: %w", 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: %w", 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 } From cd910753297f272cc7e1f79552dac2aaf142d328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 08:01:41 +0300 Subject: [PATCH 15/63] K8s delete and connect with trust-aware login and opt-in namespace removal. Add connect --ca PEM override; clear trust store on CP delete; gate K8s namespace delete behind --delete-namespace (never default). --- internal/cmd/connect.go | 1 + internal/cmd/delete.go | 3 ++ internal/cmd/delete_all.go | 4 ++- internal/connect/controlplane/connect.go | 24 +++++-------- internal/connect/controlplane/k8s/k8s.go | 27 +++++--------- .../connect/controlplane/remote/remote.go | 14 ++++---- internal/connect/execute.go | 28 ++++++++------- internal/delete/all/all.go | 12 +++---- internal/delete/controlplane/factory.go | 4 +-- internal/delete/controlplane/k8s/k8s.go | 23 ++++++------ internal/delete/controlplane/local/local.go | 2 +- internal/delete/controlplane/remote/remote.go | 2 +- internal/delete/execute.go | 11 +++--- internal/deploy/execute.go | 4 +-- internal/execute/utils.go | 36 ++++++++++--------- internal/get/controllers.go | 5 --- 16 files changed, 95 insertions(+), 105 deletions(-) diff --git a/internal/cmd/connect.go b/internal/cmd/connect.go index 88ad26b6e..f1821e4bc 100644 --- a/internal/cmd/connect.go +++ b/internal/cmd/connect.go @@ -48,6 +48,7 @@ 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 (connect-time override only)") return cmd } diff --git a/internal/cmd/delete.go b/internal/cmd/delete.go index ef70a4921..a2fe54369 100644 --- a/internal/cmd/delete.go +++ b/internal/cmd/delete.go @@ -21,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 == "" { @@ -62,6 +64,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_all.go b/internal/cmd/delete_all.go index e6498d100..262be16da 100644 --- a/internal/cmd/delete_all.go +++ b/internal/cmd/delete_all.go @@ -23,7 +23,9 @@ If you don't want to tear down the deployments but would like to free up the Nam 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/connect/controlplane/connect.go b/internal/connect/controlplane/connect.go index 0c8ade5d1..6f8ca1167 100644 --- a/internal/connect/controlplane/connect.go +++ b/internal/connect/controlplane/connect.go @@ -1,32 +1,24 @@ 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/pkg/util" ) -func Connect(ctrlPlane rsc.ControlPlane, endpoint string, ns *rsc.Namespace) error { - // Connect to Controller - baseURL, err := util.GetBaseURL(endpoint) - if err != nil { - return err - } +func Connect(ctrlPlane rsc.ControlPlane, endpoint, namespace, caFile 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, caFile, 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 bb8ce5967..1ea8da239 100644 --- a/internal/connect/controlplane/k8s/k8s.go +++ b/internal/connect/controlplane/k8s/k8s.go @@ -13,12 +13,14 @@ import ( type kubernetesExecutor struct { controlPlane *rsc.KubernetesControlPlane namespace string + caFile string } -func newKubernetesExecutor(controlPlane *rsc.KubernetesControlPlane, namespace string) *kubernetesExecutor { +func newKubernetesExecutor(controlPlane *rsc.KubernetesControlPlane, namespace, caFile string) *kubernetesExecutor { return &kubernetesExecutor{ controlPlane: controlPlane, namespace: namespace, + caFile: caFile, } } @@ -26,7 +28,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 string) (execute.Executor, error) { controlPlane := &rsc.KubernetesControlPlane{ IofogUser: rsc.IofogUser{Email: email, Password: password}, KubeConfig: kubeConfig, @@ -35,11 +37,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), 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 string) (execute.Executor, error) { controlPlane, err := rsc.UnmarshallKubernetesControlPlane(yaml) if err != nil { return nil, err @@ -49,43 +50,33 @@ func NewExecutor(namespace, name string, yaml []byte, kind config.Kind) (execute return nil, err } - return newKubernetesExecutor(&controlPlane, namespace), nil + return newKubernetesExecutor(&controlPlane, namespace, caFile), 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) + err = connectcontrolplane.Connect(exe.controlPlane, endpoint, exe.namespace, exe.caFile, ns) if err != nil { return } @@ -105,7 +96,6 @@ func (exe *kubernetesExecutor) Execute() (err error) { } exe.controlPlane.Endpoint = endpoint - // Save changes ns.SetControlPlane(exe.controlPlane) return config.Flush() } @@ -120,7 +110,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/remote.go b/internal/connect/controlplane/remote/remote.go index 689b249f5..74b06d71d 100644 --- a/internal/connect/controlplane/remote/remote.go +++ b/internal/connect/controlplane/remote/remote.go @@ -15,9 +15,10 @@ import ( type remoteExecutor struct { controlPlane *rsc.RemoteControlPlane namespace string + caFile string } -func NewManualExecutor(namespace, name, endpoint, email, password string) (execute.Executor, error) { +func NewManualExecutor(namespace, name, endpoint, email, password, caFile string) (execute.Executor, error) { fmtEndpoint, err := formatEndpoint(endpoint) if err != nil { return nil, err @@ -38,10 +39,10 @@ func NewManualExecutor(namespace, name, endpoint, email, password string) (execu }, } - return newRemoteExecutor(controlPlane, namespace), nil + return newRemoteExecutor(controlPlane, namespace, caFile), 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 string) (execute.Executor, error) { // Read the input file controlPlane, err := rsc.UnmarshallRemoteControlPlane(yaml) if err != nil { @@ -74,13 +75,14 @@ func NewExecutor(namespace, name string, yaml []byte, kind config.Kind) (execute } } - return newRemoteExecutor(&controlPlane, namespace), nil + return newRemoteExecutor(&controlPlane, namespace, caFile), nil } -func newRemoteExecutor(controlPlane *rsc.RemoteControlPlane, namespace string) *remoteExecutor { +func newRemoteExecutor(controlPlane *rsc.RemoteControlPlane, namespace, caFile string) *remoteExecutor { r := &remoteExecutor{ controlPlane: controlPlane, namespace: namespace, + caFile: caFile, } return r } @@ -103,7 +105,7 @@ func (exe *remoteExecutor) Execute() (err error) { if err != nil { return err } - err = connectcontrolplane.Connect(exe.controlPlane, endpoint, ns) + err = connectcontrolplane.Connect(exe.controlPlane, endpoint, exe.namespace, exe.caFile, ns) if err != nil { return err } diff --git a/internal/connect/execute.go b/internal/connect/execute.go index a8dd11245..56a837bff 100644 --- a/internal/connect/execute.go +++ b/internal/connect/execute.go @@ -23,6 +23,7 @@ type Options struct { IofogUserPass string Generate bool Base64Encoded bool + CAFile string } var kindOrder = []config.Kind{ @@ -30,13 +31,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 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) + }, + config.RemoteControlPlaneKind: func(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { + return connectremotecontrolplane.NewExecutor(opt.Namespace, opt.Name, opt.YAML, config.RemoteControlPlaneKind, caFile) + }, + } } func Execute(opt *Options) error { @@ -78,7 +81,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) } return manualExecute(opt) } @@ -91,12 +94,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) 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) if err != nil { return err } @@ -109,8 +112,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 string) error { + handlers := buildKindHandlers(caFile) + executorsMap, err := execute.GetExecutorsFromYAML(yamlFile, namespace, handlers, false) if err != nil { return err } diff --git a/internal/delete/all/all.go b/internal/delete/all/all.go index 222b3830e..7774c7257 100644 --- a/internal/delete/all/all.go +++ b/internal/delete/all/all.go @@ -10,7 +10,7 @@ import ( "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() @@ -38,19 +38,19 @@ 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 } } @@ -76,7 +76,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/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 3dcb13c4e..eda5101b2 100644 --- a/internal/delete/controlplane/k8s/k8s.go +++ b/internal/delete/controlplane/k8s/k8s.go @@ -4,29 +4,29 @@ 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 @@ -41,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 564b7baaa..9333df104 100644 --- a/internal/delete/controlplane/local/local.go +++ b/internal/delete/controlplane/local/local.go @@ -24,7 +24,7 @@ func (exe *Executor) GetName() string { return "Delete Control Plane" } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() (err error) { // Get Control Plane ns, err := config.GetNamespace(exe.namespace) diff --git a/internal/delete/controlplane/remote/remote.go b/internal/delete/controlplane/remote/remote.go index d22a23128..2dca2b7c1 100644 --- a/internal/delete/controlplane/remote/remote.go +++ b/internal/delete/controlplane/remote/remote.go @@ -24,7 +24,7 @@ func (exe *Executor) GetName() string { return "Delete Control Plane" } -// Execute deletes application by deleting its associated flow +// Execute deletes application by deleting its associated application func (exe *Executor) Execute() (err error) { // Get Control Plane ns, err := config.GetNamespace(exe.namespace) diff --git a/internal/delete/execute.go b/internal/delete/execute.go index 54b5a4b08..1f4e4c982 100644 --- a/internal/delete/execute.go +++ b/internal/delete/execute.go @@ -30,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{ @@ -69,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) @@ -134,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 } diff --git a/internal/deploy/execute.go b/internal/deploy/execute.go index b48287593..7c2676daa 100644 --- a/internal/deploy/execute.go +++ b/internal/deploy/execute.go @@ -182,7 +182,7 @@ 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 } @@ -269,7 +269,7 @@ func Execute(opt *Options) (err error) { NatsLeafPort: &natsLeafPort, NatsClusterPort: &natsClusterPort, NatsMqttPort: &natsMqttPort, - NatsHttpPort: &natsHttpPort, + NatsHTTPPort: &natsHttpPort, JsStorageSize: &jsStorageSize, JsMemoryStoreSize: &jsMemoryStoreSize, } diff --git a/internal/execute/utils.go b/internal/execute/utils.go index 9ea07ca80..20bff9974 100644 --- a/internal/execute/utils.go +++ b/internal/execute/utils.go @@ -44,7 +44,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)) @@ -79,27 +79,29 @@ 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) { +func GetExecutorsFromYAML(inputFile, namespace string, kindHandlers map[config.Kind]func(*KindHandlerOpt) (Executor, error), deleteNamespace bool) (executorsMap map[config.Kind][]Executor, err error) { yamlFile, err := os.ReadFile(inputFile) if err != nil { return @@ -117,7 +119,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 } diff --git a/internal/get/controllers.go b/internal/get/controllers.go index 11b62fe45..becf1df98 100644 --- a/internal/get/controllers.go +++ b/internal/get/controllers.go @@ -137,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 From f5db8776b81e976e71e20a3f4f206cc19f939225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 08:01:46 +0300 Subject: [PATCH 16/63] Migrate CLI paths from Flow API to Application API and agent arch fields. Update get, describe, delete, deploy, and catalog callers for SDK v3.8; replace FogType with arch and fix NATS HTTP port field naming. --- internal/delete/application/legacy.go | 8 +- internal/delete/application/remote.go | 10 +- internal/delete/catalogitem/catalog_item.go | 2 +- internal/delete/certificate/certificate.go | 2 +- internal/delete/configmap/config_map.go | 2 +- internal/delete/microservice/microservice.go | 2 +- internal/delete/namespace/namespace.go | 2 +- internal/delete/registry/registry.go | 2 +- internal/delete/secret/secret.go | 2 +- internal/delete/service/service.go | 2 +- internal/delete/template/execute.go | 2 +- internal/delete/volume/volume.go | 2 +- internal/delete/volumemount/volume_mount.go | 2 +- internal/deploy/agent/remote.go | 38 +++- internal/deploy/agentconfig/utils.go | 18 +- internal/deploy/airgap/helpers.go | 22 +- internal/deploy/airgap/images.go | 202 ++++++++++++++---- internal/deploy/catalogitem/catalog_item.go | 32 ++- internal/deploy/controller/local/local.go | 10 +- internal/deploy/controller/remote/remote.go | 30 ++- internal/deploy/controlplane/local/execute.go | 24 +-- .../deploy/controlplane/remote/execute.go | 83 +++---- internal/deploy/offlineimage/executor.go | 42 +++- internal/deploy/offlineimage/helpers.go | 14 +- internal/describe/application.go | 26 +-- internal/describe/application_legacy.go | 4 +- internal/describe/utils.go | 48 +++-- internal/get/applications.go | 26 +-- internal/get/applications_legacy.go | 10 +- internal/get/catalog.go | 18 +- internal/get/system_applications.go | 24 +-- internal/rename/application/executor.go | 21 +- internal/start/application/application.go | 4 +- internal/stop/application/application.go | 4 +- internal/util/client/api.go | 14 +- 35 files changed, 462 insertions(+), 292 deletions(-) diff --git a/internal/delete/application/legacy.go b/internal/delete/application/legacy.go index 3ee557ca0..a59d3b0ca 100644 --- a/internal/delete/application/legacy.go +++ b/internal/delete/application/legacy.go @@ -1,11 +1,11 @@ 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 } @@ -15,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 d4afd392b..907b230e9 100644 --- a/internal/delete/application/remote.go +++ b/internal/delete/application/remote.go @@ -10,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) { @@ -38,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 { diff --git a/internal/delete/catalogitem/catalog_item.go b/internal/delete/catalogitem/catalog_item.go index c514a07b4..e7517c558 100644 --- a/internal/delete/catalogitem/catalog_item.go +++ b/internal/delete/catalogitem/catalog_item.go @@ -25,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 4e543f46f..aa97999f8 100644 --- a/internal/delete/certificate/certificate.go +++ b/internal/delete/certificate/certificate.go @@ -30,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 b343942e1..5c1955430 100644 --- a/internal/delete/configmap/config_map.go +++ b/internal/delete/configmap/config_map.go @@ -25,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/microservice/microservice.go b/internal/delete/microservice/microservice.go index 8fe5352b7..1d9d075e7 100644 --- a/internal/delete/microservice/microservice.go +++ b/internal/delete/microservice/microservice.go @@ -25,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 0ac14b58c..666c144e3 100644 --- a/internal/delete/namespace/namespace.go +++ b/internal/delete/namespace/namespace.go @@ -29,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 687f21fa0..021f7f71e 100644 --- a/internal/delete/registry/registry.go +++ b/internal/delete/registry/registry.go @@ -31,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/secret/secret.go b/internal/delete/secret/secret.go index f60fb6713..67808037b 100644 --- a/internal/delete/secret/secret.go +++ b/internal/delete/secret/secret.go @@ -25,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 f7cb1dfc7..37ad2f64a 100644 --- a/internal/delete/service/service.go +++ b/internal/delete/service/service.go @@ -25,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/template/execute.go b/internal/delete/template/execute.go index 39189a601..dfa831ce5 100644 --- a/internal/delete/template/execute.go +++ b/internal/delete/template/execute.go @@ -39,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/volume.go b/internal/delete/volume/volume.go index 27bce8c80..035e622af 100644 --- a/internal/delete/volume/volume.go +++ b/internal/delete/volume/volume.go @@ -30,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 b74f442b5..de67bca7d 100644 --- a/internal/delete/volumemount/volume_mount.go +++ b/internal/delete/volumemount/volume_mount.go @@ -25,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/remote.go b/internal/deploy/agent/remote.go index 657d7a92a..6ae4ee04d 100644 --- a/internal/deploy/agent/remote.go +++ b/internal/deploy/agent/remote.go @@ -102,12 +102,12 @@ func (exe *remoteExecutor) ProvisionAgent() (string, error) { 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 { + var arch *string + if agentConfig.Arch == nil { auto := "auto" - fogType = &auto + arch = &auto } else { - fogType = agentConfig.FogType + arch = agentConfig.Arch } err = agent.SetInitialConfig( agentConfig.Name, @@ -115,7 +115,7 @@ func (exe *remoteExecutor) ProvisionAgent() (string, error) { // agentConfig.Latitude, // agentConfig.Longitude, // agentConfig.Description, - *fogType, + *arch, agentConfig.AgentConfiguration, // Pass the embedded client.AgentConfiguration ) if err != nil { @@ -214,7 +214,7 @@ func (exe *remoteExecutor) Execute() (err error) { } // Resolve platform and container engine - platform, err := deployairgap.ResolvePlatform(exe.agent.Config.FogType) + platform, err := deployairgap.ResolvePlatform(exe.agent.Config.Arch) if err != nil { return fmt.Errorf("failed to resolve platform: %w", err) } @@ -257,11 +257,29 @@ func (exe *remoteExecutor) Execute() (err error) { if routerImage != "" { imageList = append(imageList, routerImage) } - if images.Nats != "" { - imageList = append(imageList, images.Nats) + if images.NatsAMD64 != "" { + imageList = append(imageList, images.NatsAMD64) } - if images.Debugger != "" { - imageList = append(imageList, images.Debugger) + if images.DebuggerAMD64 != "" { + imageList = append(imageList, images.DebuggerAMD64) + } + if images.NatsARM64 != "" { + imageList = append(imageList, images.NatsARM64) + } + if images.DebuggerARM64 != "" { + imageList = append(imageList, images.DebuggerARM64) + } + if images.NatsRISCV64 != "" { + imageList = append(imageList, images.NatsRISCV64) + } + if images.DebuggerRISCV64 != "" { + imageList = append(imageList, images.DebuggerRISCV64) + } + if images.NatsARM != "" { + imageList = append(imageList, images.NatsARM) + } + if images.DebuggerARM != "" { + imageList = append(imageList, images.DebuggerARM) } // Transfer images before bootstrap diff --git a/internal/deploy/agentconfig/utils.go b/internal/deploy/agentconfig/utils.go index 266bd05e1..bf776447a 100644 --- a/internal/deploy/agentconfig/utils.go +++ b/internal/deploy/agentconfig/utils.go @@ -125,20 +125,20 @@ 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] + var archPtr *int64 + if agentConfig.Arch != nil { + arch, found := rsc.ArchStringMap[*agentConfig.Arch] if !found { - fogType = 0 + arch = 0 } - fogTypePtr = &fogType + archPtr = &arch } request.Location = agentConfig.Location request.Latitude = agentConfig.Latitude request.Longitude = agentConfig.Longitude request.Description = agentConfig.Description request.Name = agentConfig.Name - request.FogType = fogTypePtr + request.ArchID = archPtr request.AgentConfiguration = agentConfig.AgentConfiguration request.Tags = tags return @@ -152,9 +152,9 @@ func createAgentFromConfiguration(agentConfig *rsc.AgentConfiguration, tags *[]s if createAgentRequest.Name == "" { createAgentRequest.Name = name } - if createAgentRequest.FogType == nil { - fogType := int64(0) - createAgentRequest.FogType = &fogType + if createAgentRequest.ArchID == nil { + arch := int64(0) + createAgentRequest.ArchID = &arch } agent, err := clt.CreateAgent(createAgentRequest) if err != nil { diff --git a/internal/deploy/airgap/helpers.go b/internal/deploy/airgap/helpers.go index 5b08580e6..c32743054 100644 --- a/internal/deploy/airgap/helpers.go +++ b/internal/deploy/airgap/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 ContainerEngine string @@ -23,18 +25,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") } - 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) } } @@ -81,9 +83,9 @@ func ValidateAirgapRequirements(agentConfig *rsc.AgentConfiguration) error { 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)") + // Validate Arch + 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 @@ -95,7 +97,7 @@ func ValidateAirgapRequirements(agentConfig *rsc.AgentConfiguration) error { } // 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 { diff --git a/internal/deploy/airgap/images.go b/internal/deploy/airgap/images.go index 3621bcafa..3fa5f243e 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,148 @@ 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.RouterX86 = util.GetRouterImage() + images.RouterAMD64 = util.GetRouterImage() + } + } + if images.RouterARM64 == "" { + if sm.Router.ARM != "" { + images.RouterARM64 = sm.Router.ARM64 + } else { + 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 == "" { + if images.DebuggerAMD64 != "" { + images.DebuggerAMD64 = images.DebuggerAMD64 + } else { + images.DebuggerAMD64 = util.GetDebuggerImage() + } + } + if images.DebuggerARM64 == "" { + if images.DebuggerARM64 != "" { + images.DebuggerARM64 = images.DebuggerARM64 + } else { + images.DebuggerARM64 = util.GetDebuggerImage() + } + } + if images.DebuggerRISCV64 == "" { + if images.DebuggerRISCV64 != "" { + images.DebuggerRISCV64 = images.DebuggerRISCV64 + } else { + images.DebuggerRISCV64 = util.GetDebuggerImage() + } + } + if images.DebuggerARM == "" { + if images.DebuggerARM != "" { + images.DebuggerARM = images.DebuggerARM + } else { + 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 == "" { + if images.DebuggerARM64 != "" { + images.DebuggerARM64 = images.DebuggerARM64 + } else { + images.DebuggerARM64 = util.GetDebuggerImage() + } + } + if images.DebuggerRISCV64 == "" { + if images.DebuggerRISCV64 != "" { + images.DebuggerRISCV64 = images.DebuggerRISCV64 + } else { + images.DebuggerRISCV64 = util.GetDebuggerImage() + } + } + if images.DebuggerARM == "" { + if images.DebuggerARM != "" { + images.DebuggerARM = images.DebuggerARM + } else { + images.DebuggerARM = util.GetDebuggerImage() + } } } @@ -231,8 +341,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/catalogitem/catalog_item.go b/internal/deploy/catalogitem/catalog_item.go index 3c1a5fb58..d05293e5f 100644 --- a/internal/deploy/catalogitem/catalog_item.go +++ b/internal/deploy/catalogitem/catalog_item.go @@ -29,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 } @@ -52,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.X86, - AgentTypeID: client.AgentTypeAgentTypeIDDict["x86"], + 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.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"], }) } @@ -77,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, @@ -143,7 +159,7 @@ func validate(opt *apps.CatalogItem) error { return err } - if opt.ARM == "" && opt.X86 == "" { + if opt.ARM == "" && opt.ARM64 == "" && opt.RISCV64 == "" && opt.ARM == "" { return util.NewInputError("At least one image must be specified") } diff --git a/internal/deploy/controller/local/local.go b/internal/deploy/controller/local/local.go index 343d8858d..7ba1b179f 100644 --- a/internal/deploy/controller/local/local.go +++ b/internal/deploy/controller/local/local.go @@ -92,15 +92,7 @@ func newExecutor(namespace string, controlPlane *rsc.LocalControlPlane, ctrl *rs 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{ + }, install.Auth(rsc.AuthToCPV3(controlPlane.Auth)), install.Database{ Provider: controlPlane.Database.Provider, Host: controlPlane.Database.Host, Port: controlPlane.Database.Port, diff --git a/internal/deploy/controller/remote/remote.go b/internal/deploy/controller/remote/remote.go index 98921e17a..8a8ae8212 100644 --- a/internal/deploy/controller/remote/remote.go +++ b/internal/deploy/controller/remote/remote.go @@ -161,11 +161,7 @@ func (exe *remoteExecutor) Execute() (err error) { deployer.SetControllerExternalDatabase(db.Host, db.User, db.Password, db.Provider, db.DatabaseName, db.Port, db.SSL, db.CA) } - 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) - } - + // v3.8 auth is embedded/external OIDC — configured via ControlPlane spec, not Keycloak env. // Set events configuration if present if exe.controlPlane.Events.AuditEnabled != nil { auditEnabled := *exe.controlPlane.Events.AuditEnabled @@ -196,14 +192,28 @@ func (exe *remoteExecutor) Execute() (err error) { } func (exe *remoteExecutor) setDefaultValues() { - if exe.controlPlane.SystemMicroservices.Router.X86 == "" { - exe.controlPlane.SystemMicroservices.Router.X86 = util.GetRouterImage() + if exe.controlPlane.SystemMicroservices.Router.AMD64 == "" { + exe.controlPlane.SystemMicroservices.Router.AMD64 = util.GetRouterImage() + } + if exe.controlPlane.SystemMicroservices.Router.ARM64 == "" { + exe.controlPlane.SystemMicroservices.Router.ARM64 = util.GetRouterImage() + } + if exe.controlPlane.SystemMicroservices.Router.RISCV64 == "" { + exe.controlPlane.SystemMicroservices.Router.RISCV64 = util.GetRouterImage() } if exe.controlPlane.SystemMicroservices.Router.ARM == "" { - exe.controlPlane.SystemMicroservices.Router.ARM = util.GetRouterARMImage() } - if exe.controlPlane.SystemMicroservices.Nats.X86 == "" { - exe.controlPlane.SystemMicroservices.Nats.X86 = util.GetNatsImage() + if exe.controlPlane.SystemMicroservices.Router.ARM == "" { + exe.controlPlane.SystemMicroservices.Router.ARM = util.GetRouterImage() + } + if exe.controlPlane.SystemMicroservices.Nats.AMD64 == "" { + exe.controlPlane.SystemMicroservices.Nats.AMD64 = util.GetNatsImage() + } + if exe.controlPlane.SystemMicroservices.Nats.ARM64 == "" { + exe.controlPlane.SystemMicroservices.Nats.ARM64 = util.GetNatsImage() + } + if exe.controlPlane.SystemMicroservices.Nats.RISCV64 == "" { + exe.controlPlane.SystemMicroservices.Nats.RISCV64 = util.GetNatsImage() } if exe.controlPlane.SystemMicroservices.Nats.ARM == "" { exe.controlPlane.SystemMicroservices.Nats.ARM = util.GetNatsImage() diff --git a/internal/deploy/controlplane/local/execute.go b/internal/deploy/controlplane/local/execute.go index ff24ac24d..ff0a5a022 100644 --- a/internal/deploy/controlplane/local/execute.go +++ b/internal/deploy/controlplane/local/execute.go @@ -13,9 +13,6 @@ import ( 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/pkg/util" @@ -81,25 +78,8 @@ func prepareViewerURL(endpoint string) (string, error) { 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 - } - - // Prepare viewer URL - viewerURL, err := prepareViewerURL(endpoint) - if err != nil { - return fmt.Errorf("failed to prepare viewer URL: %w", 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: %w", err) - } - +// updateViewerClientRootURL is retired in v3.8 (Keycloak viewer client). +func updateViewerClientRootURL(_ *rsc.LocalControlPlane, _ string) error { return nil } diff --git a/internal/deploy/controlplane/remote/execute.go b/internal/deploy/controlplane/remote/execute.go index 158ba7228..e3c4cc3ce 100644 --- a/internal/deploy/controlplane/remote/execute.go +++ b/internal/deploy/controlplane/remote/execute.go @@ -57,8 +57,8 @@ func applySystemAgentNatsDefaults(cfg *rsc.AgentConfiguration) { if cfg.NatsMqttPort == nil { cfg.NatsMqttPort = iutil.MakeIntPtr(defaultNatsMqttPort) } - if cfg.NatsHttpPort == nil { - cfg.NatsHttpPort = iutil.MakeIntPtr(defaultNatsHttpPort) + if cfg.NatsHTTPPort == nil { + cfg.NatsHTTPPort = iutil.MakeIntPtr(defaultNatsHttpPort) } if cfg.JsStorageSize == nil { cfg.JsStorageSize = iutil.MakeStrPtr(defaultJsStorageSize) @@ -126,8 +126,8 @@ func deploySystemAgent(namespace string, ctrl *rsc.RemoteController, systemAgent upstreamNatsServers := []string{} deployAgentConfig = rsc.AgentConfiguration{ - Name: ctrl.Name, - FogType: iutil.MakeStrPtr("auto"), + Name: ctrl.Name, + Arch: iutil.MakeStrPtr("auto"), AgentConfiguration: client.AgentConfiguration{ IsSystem: iutil.MakeBoolPtr(true), DeploymentType: iutil.MakeStrPtr(deploymentType), @@ -274,8 +274,8 @@ func deployNextSystemAgent(namespace string, ctrl *rsc.RemoteController, systemA upstreamRouters := []string{"default-router"} deployAgentConfig = rsc.AgentConfiguration{ - Name: ctrl.Name, - FogType: iutil.MakeStrPtr("auto"), + Name: ctrl.Name, + Arch: iutil.MakeStrPtr("auto"), AgentConfiguration: client.AgentConfiguration{ IsSystem: iutil.MakeBoolPtr(true), DeploymentType: iutil.MakeStrPtr(deploymentType), @@ -387,32 +387,8 @@ func prepareViewerURL(endpoint string) (string, error) { 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: %w", 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: %w", err) - } - +// updateViewerClientRootURL is retired in v3.8 (Keycloak viewer client). +func updateViewerClientRootURL(_ *rsc.RemoteControlPlane, _ string) error { return nil } @@ -766,8 +742,17 @@ func (exe remoteControlPlaneExecutor) transferControllerImages() error { // 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) + if images.NatsAMD64 != "" { + imageList = append(imageList, images.NatsAMD64) + } + if images.NatsARM64 != "" { + imageList = append(imageList, images.NatsARM64) + } + if images.NatsRISCV64 != "" { + imageList = append(imageList, images.NatsRISCV64) + } + if images.NatsARM != "" { + imageList = append(imageList, images.NatsARM) } controllers := remoteControlPlane.GetControllers() @@ -777,7 +762,7 @@ func (exe remoteControlPlaneExecutor) transferControllerImages() error { if !ok { return util.NewInternalError("Could not convert Controller to Remote Controller") } - platform, err := deployairgap.ResolvePlatform(controller.SystemAgent.AgentConfiguration.FogType) + platform, err := deployairgap.ResolvePlatform(controller.SystemAgent.AgentConfiguration.Arch) if err != nil { return fmt.Errorf("controller %s: %w", controller.Name, err) } @@ -824,7 +809,7 @@ func (exe remoteControlPlaneExecutor) transferSystemAgentImages() error { } // Resolve platform and container engine - platform, err := deployairgap.ResolvePlatform(controller.SystemAgent.AgentConfiguration.FogType) + platform, err := deployairgap.ResolvePlatform(controller.SystemAgent.AgentConfiguration.Arch) if err != nil { return fmt.Errorf("system agent for controller %s: %w", controller.Name, err) } @@ -860,11 +845,29 @@ func (exe remoteControlPlaneExecutor) transferSystemAgentImages() error { if routerImage != "" { imageList = append(imageList, routerImage) } - if images.Nats != "" { - imageList = append(imageList, images.Nats) + if images.NatsAMD64 != "" { + imageList = append(imageList, images.NatsAMD64) + } + if images.NatsARM64 != "" { + imageList = append(imageList, images.NatsARM64) + } + if images.NatsRISCV64 != "" { + imageList = append(imageList, images.NatsRISCV64) + } + if images.NatsARM != "" { + imageList = append(imageList, images.NatsARM) + } + if images.DebuggerAMD64 != "" { + imageList = append(imageList, images.DebuggerAMD64) + } + if images.DebuggerARM64 != "" { + imageList = append(imageList, images.DebuggerARM64) + } + if images.DebuggerRISCV64 != "" { + imageList = append(imageList, images.DebuggerRISCV64) } - if images.Debugger != "" { - imageList = append(imageList, images.Debugger) + if images.DebuggerARM != "" { + imageList = append(imageList, images.DebuggerARM) } // Transfer images diff --git a/internal/deploy/offlineimage/executor.go b/internal/deploy/offlineimage/executor.go index 4c3a0241a..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) } @@ -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/describe/application.go b/internal/describe/application.go index 8d713ee87..094b1a5a0 100644 --- a/internal/describe/application.go +++ b/internal/describe/application.go @@ -12,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 { @@ -46,8 +46,8 @@ func (exe *applicationExecutor) init() (err error) { 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, @@ -94,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 { @@ -105,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 891696841..18a2ab18e 100644 --- a/internal/describe/application_legacy.go +++ b/internal/describe/application_legacy.go @@ -6,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/utils.go b/internal/describe/utils.go index a3bf555e9..464cf84b3 100644 --- a/internal/describe/utils.go +++ b/internal/describe/utils.go @@ -32,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) } @@ -120,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 @@ -128,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: @@ -147,15 +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 { - switch img.AgentTypeID { + switch img.ArchID { case 1: - images.X86 = img.ContainerImage + images.AMD64 = img.ContainerImage case 2: + images.ARM64 = img.ContainerImage + case 3: + images.RISCV64 = img.ContainerImage + case 4: images.ARM = img.ContainerImage } } @@ -215,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 @@ -227,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 diff --git a/internal/get/applications.go b/internal/get/applications.go index def725d43..f67c4122a 100644 --- a/internal/get/applications.go +++ b/internal/get/applications.go @@ -13,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 } @@ -63,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, @@ -95,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 { @@ -119,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 7a44cf93a..314d38881 100644 --- a/internal/get/applications_legacy.go +++ b/internal/get/applications_legacy.go @@ -3,13 +3,13 @@ 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 } @@ -20,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 a3f0198c9..00e879ee8 100644 --- a/internal/get/catalog.go +++ b/internal/get/catalog.go @@ -54,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: @@ -76,7 +80,9 @@ func tabulateCatalogItems(catalogItems []apps.CatalogItem) error { "NAME", "DESCRIPTION", "REGISTRY", - "X86", + "AMD64", + "ARM64", + "RISCV64", "ARM", } table[0] = append(table[0], headers...) @@ -88,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/system_applications.go b/internal/get/system_applications.go index 7ab9f169d..23e6e3fbd 100644 --- a/internal/get/system_applications.go +++ b/internal/get/system_applications.go @@ -11,7 +11,7 @@ import ( type systemApplicationExecutor struct { namespace string client *client.Client - flows []client.FlowInfo + applications []client.ApplicationInfo msvcsPerApplication map[int][]*client.MicroserviceInfo } @@ -58,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, @@ -89,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 { @@ -113,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/rename/application/executor.go b/internal/rename/application/executor.go index 6f676abd0..31aa62aed 100644 --- a/internal/rename/application/executor.go +++ b/internal/rename/application/executor.go @@ -3,8 +3,6 @@ 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" ) @@ -21,19 +19,16 @@ func Execute(namespace, name, newName string) error { return err } - flow, err := clt.GetFlowByName(name) + _, err = clt.GetApplicationByName(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 + // application.Name = newName + // // _, err = clt.UpdateApplication(&client.ApplicationUpdateRequest{ + // // ID: application.ID, + // // Name: &newName, + // // }) + // fmt.Println(application) + return fmt.Errorf("Application renamed not allowed") } diff --git a/internal/start/application/application.go b/internal/start/application/application.go index 215ed1ba3..ad1d313f5 100644 --- a/internal/start/application/application.go +++ b/internal/start/application/application.go @@ -32,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/stop/application/application.go b/internal/stop/application/application.go index 3b3bdaf89..490be7ddc 100644 --- a/internal/stop/application/application.go +++ b/internal/stop/application/application.go @@ -32,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/util/client/api.go b/internal/util/client/api.go index 735831f14..866275327 100644 --- a/internal/util/client/api.go +++ b/internal/util/client/api.go @@ -165,9 +165,9 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura agentMapByUUID[agent.UUID] = *agent } - fogType, found := rsc.FogTypeIntMap[agentInfo.FogType] + arch, found := rsc.ArchIntMap[agentInfo.ArchID] if !found { - fogType = "auto" + arch = "auto" } routerConfig := client.RouterConfig{ @@ -193,7 +193,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, } @@ -219,10 +219,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, @@ -243,7 +243,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, @@ -274,8 +274,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, From 9abf9e0efb3cd3255ae071c68c8116b5a64d0675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 08:02:06 +0300 Subject: [PATCH 17/63] Update k8s.bats deploy YAML for v3.8 embedded auth and image set. Drop retired portManager/proxy images; assert publicUrl and consoleUrl in describe output. --- test/bats/k8s.bats | 72 ++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 41 deletions(-) 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 From 3117798e8988131204caea8c41cdb2e57aff5e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 23:47:16 +0300 Subject: [PATCH 18/63] Split edgelet binary and image versions and refresh build dependencies. Add edgelet release ldflags, bump controller to 3.8.0-rc.2, migrate to go.podman.io/image and moby client, and drop Azure pipeline bumps from bump.sh. --- .golangci.yaml | 2 +- Makefile | 11 +- go.mod | 146 ++++++------- go.sum | 499 ++++++++++++++++++-------------------------- pkg/util/version.go | 26 ++- script/bump.sh | 12 +- versions.mk | 5 +- 7 files changed, 298 insertions(+), 403 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 1fec21bfe..1cbc7a1a7 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -115,7 +115,7 @@ linters: - github.com/mitchellh/go-homedir - github.com/pkg - github.com/twmb/algoimpl - - github.com/containers/image + - go.podman.io/image - github.com/opencontainers/go-digest - github.com/gorilla/websocket - github.com/vmihailenco/msgpack diff --git a/Makefile b/Makefile index 803c2dc04..13c08bfd8 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,8 @@ ifeq ($(FLAVOR),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 @@ -43,6 +45,8 @@ else 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) @@ -58,9 +62,12 @@ 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_VERSION) +LDFLAGS += -X $(PREFIX).edgeletTag=$(EDGELET_IMAGE_TAG) LDFLAGS += -X $(PREFIX).controllerVersion=$(CONTROLLER_VERSION) -LDFLAGS += -X $(PREFIX).edgeletVersion=$(EDGELET_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 GOLANGCI_LINT_VERSION ?= v2.12.2 diff --git a/go.mod b/go.mod index 962e5aee4..d33e34ee3 100644 --- a/go.mod +++ b/go.mod @@ -4,20 +4,20 @@ go 1.26.4 require ( 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.8.0-rc.3 + github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.4 github.com/eclipse-iofog/iofog-operator/v3 v3.8.0-rc.1 github.com/gorilla/websocket v1.5.3 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/stretchr/testify v1.10.0 + 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 + go.podman.io/image/v5 v5.40.0 golang.org/x/crypto v0.51.0 golang.org/x/sys v0.45.0 golang.org/x/term v0.43.0 @@ -31,124 +31,106 @@ require ( ) 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/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/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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/proglottis/gpgme v0.1.3 // indirect - github.com/rivo/uniseg v0.4.7 // 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/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 + 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.27.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.37.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 + 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 @@ -159,7 +141,7 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect ) -replace github.com/eclipse-iofog/iofog-go-sdk/v3 => github.com/Datasance/iofog-go-sdk/v3 v3.8.0-rc.3 +replace github.com/eclipse-iofog/iofog-go-sdk/v3 => github.com/Datasance/iofog-go-sdk/v3 v3.8.0-rc.4 exclude github.com/Sirupsen/logrus v1.4.2 diff --git a/go.sum b/go.sum index 9932cbd51..cd26a0881 100644 --- a/go.sum +++ b/go.sum @@ -1,158 +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/Datasance/iofog-go-sdk/v3 v3.8.0-rc.3 h1:Ye2ZhAmXAYK6xiKmPXB23JA/GifRxJAPzw7r/Zin06Q= -github.com/Datasance/iofog-go-sdk/v3 v3.8.0-rc.3/go.mod h1:MU+YPxFRGFzBMboi1khtEGAoRwaF4yJX3KuBwFcfCKg= +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/Datasance/iofog-go-sdk/v3 v3.8.0-rc.4 h1:2RdokTAstIB/290zu4F1ubchi/o6+yzgv7WQkAzeCoo= +github.com/Datasance/iofog-go-sdk/v3 v3.8.0-rc.4/go.mod h1:MU+YPxFRGFzBMboi1khtEGAoRwaF4yJX3KuBwFcfCKg= 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/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/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-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= @@ -160,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= @@ -168,26 +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/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= @@ -196,60 +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/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= @@ -259,206 +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/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.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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= -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/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.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.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/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.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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-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.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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.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.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= 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.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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -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/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.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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= 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= @@ -468,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= @@ -491,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/pkg/util/version.go b/pkg/util/version.go index 003e0de24..4e3845c3b 100644 --- a/pkg/util/version.go +++ b/pkg/util/version.go @@ -18,14 +18,17 @@ var ( packageRepoBase = "https://iofog.datasance.com" ociSourceRepo = "https://github.com/eclipse-iofog/iofogctl" - controllerTag = "undefined" - operatorTag = "undefined" - routerTag = "undefined" - natsTag = "undefined" - edgeletTag = "undefined" - controllerVersion = "undefined" - edgeletVersion = "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 ( @@ -63,8 +66,11 @@ 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 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", imageRegistry, controllerImage, controllerTag) diff --git a/script/bump.sh b/script/bump.sh index 1533ebe84..04300b4ae 100755 --- a/script/bump.sh +++ b/script/bump.sh @@ -24,20 +24,12 @@ 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/(.*iofog-go-sdk\/v3@).*/\1v$version/g" Makefile +sed -i.bkp -E "s/(.*iofog-operator\/v3@).*/\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/versions.mk b/versions.mk index 586331c8d..6696014ae 100644 --- a/versions.mk +++ b/versions.mk @@ -1,5 +1,6 @@ OPERATOR_VERSION ?= 3.8.0-rc.1 -CONTROLLER_VERSION ?= 3.8.0-rc.1 +CONTROLLER_VERSION ?= 3.8.0-rc.2 ROUTER_VERSION ?= 3.8.0-rc.1 NATS_VERSION ?= 2.14.2-rc.1 -EDGELET_VERSION ?= 3.8.0-rc.2 +EDGELET_BINARY_VERSION ?= v1.0.0-rc.3 +EDGELET_IMAGE_TAG ?= 1.0.0-rc.3 From a7f266420932662ccba6462735dbd598d796cfaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 23:47:23 +0300 Subject: [PATCH 19/63] Replace legacy Java agent install scripts with embedded edgelet assets. Remove agent, airgap-agent, and container-agent script bundles and embed the edgelet script tree instead. --- assets/agent/check_prereqs.sh | 9 - assets/agent/init.sh | 296 -- assets/agent/install_container_engine.sh | 284 -- assets/agent/install_deps.sh | 6 - assets/agent/install_iofog.sh | 102 - assets/agent/install_java.sh | 74 - assets/agent/uninstall_iofog.sh | 37 - assets/airgap-agent/check_prereqs.sh | 9 - assets/airgap-agent/init.sh | 296 -- .../airgap-agent/install_container_engine.sh | 183 - assets/airgap-agent/install_deps.sh | 6 - assets/airgap-agent/install_iofog.sh | 295 -- assets/airgap-agent/uninstall_iofog.sh | 107 - assets/container-agent/check_prereqs.sh | 9 - assets/container-agent/init.sh | 296 -- .../install_container_engine.sh | 302 -- assets/container-agent/install_deps.sh | 6 - assets/container-agent/install_iofog.sh | 339 -- assets/container-agent/uninstall_iofog.sh | 109 - assets/edgelet/edgelet-config.yaml | 49 + assets/edgelet/edgelet-controller-ca.crt | 3728 +++++++++++++++++ assets/edgelet/scripts/bundled.sh | 51 + assets/edgelet/scripts/check_prereqs.sh | 15 + .../scripts/configure_container_edgelet.sh | 54 + .../scripts/configure_container_engine.sh | 111 + assets/edgelet/scripts/detect_init.sh | 101 + assets/edgelet/scripts/install.sh | 208 + assets/edgelet/scripts/install_container.sh | 75 + assets/edgelet/scripts/install_deps.sh | 25 + assets/edgelet/scripts/install_init_units.sh | 760 ++++ assets/edgelet/scripts/lib/binary.sh | 81 + assets/edgelet/scripts/lib/common.sh | 46 + assets/edgelet/scripts/lib/container_cli.sh | 65 + .../edgelet/scripts/lib/container_engine.sh | 68 + .../edgelet/scripts/lib/container_mounts.sh | 35 + assets/edgelet/scripts/lib/paths.sh | 89 + assets/edgelet/scripts/lib/receipt.sh | 94 + assets/edgelet/scripts/start_edgelet.sh | 183 + assets/edgelet/scripts/uninstall.sh | 314 ++ assets/edgelet/scripts/wait_edgelet_ready.sh | 106 + assets/embed.go | 2 +- 41 files changed, 6259 insertions(+), 2766 deletions(-) delete mode 100755 assets/agent/check_prereqs.sh delete mode 100755 assets/agent/init.sh delete mode 100755 assets/agent/install_container_engine.sh delete mode 100755 assets/agent/install_deps.sh delete mode 100755 assets/agent/install_iofog.sh delete mode 100755 assets/agent/install_java.sh delete mode 100755 assets/agent/uninstall_iofog.sh delete mode 100755 assets/airgap-agent/check_prereqs.sh delete mode 100755 assets/airgap-agent/init.sh delete mode 100644 assets/airgap-agent/install_container_engine.sh delete mode 100755 assets/airgap-agent/install_deps.sh delete mode 100644 assets/airgap-agent/install_iofog.sh delete mode 100755 assets/airgap-agent/uninstall_iofog.sh delete mode 100755 assets/container-agent/check_prereqs.sh delete mode 100755 assets/container-agent/init.sh delete mode 100755 assets/container-agent/install_container_engine.sh delete mode 100755 assets/container-agent/install_deps.sh delete mode 100755 assets/container-agent/install_iofog.sh delete mode 100755 assets/container-agent/uninstall_iofog.sh create mode 100644 assets/edgelet/edgelet-config.yaml create mode 100644 assets/edgelet/edgelet-controller-ca.crt create mode 100644 assets/edgelet/scripts/bundled.sh create mode 100755 assets/edgelet/scripts/check_prereqs.sh create mode 100644 assets/edgelet/scripts/configure_container_edgelet.sh create mode 100755 assets/edgelet/scripts/configure_container_engine.sh create mode 100755 assets/edgelet/scripts/detect_init.sh create mode 100644 assets/edgelet/scripts/install.sh create mode 100644 assets/edgelet/scripts/install_container.sh create mode 100755 assets/edgelet/scripts/install_deps.sh create mode 100755 assets/edgelet/scripts/install_init_units.sh create mode 100644 assets/edgelet/scripts/lib/binary.sh create mode 100644 assets/edgelet/scripts/lib/common.sh create mode 100644 assets/edgelet/scripts/lib/container_cli.sh create mode 100644 assets/edgelet/scripts/lib/container_engine.sh create mode 100644 assets/edgelet/scripts/lib/container_mounts.sh create mode 100644 assets/edgelet/scripts/lib/paths.sh create mode 100644 assets/edgelet/scripts/lib/receipt.sh create mode 100755 assets/edgelet/scripts/start_edgelet.sh create mode 100755 assets/edgelet/scripts/uninstall.sh create mode 100644 assets/edgelet/scripts/wait_edgelet_ready.sh 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/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/edgelet/edgelet-config.yaml b/assets/edgelet/edgelet-config.yaml new file mode 100644 index 000000000..7518a57af --- /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: "30" + 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..5bbe8433e --- /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.3 +# sudo ./install.sh --airgap --bin-path=/path/to/edgelet-linux-amd64 --version=v1.0.0-rc.3 +# sudo ./install.sh --upgrade --version=v1.0.0-rc.3 +# 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 index 14a000660..a1bf1ea7d 100644 --- a/assets/embed.go +++ b/assets/embed.go @@ -4,5 +4,5 @@ import "embed" // FS holds install scripts and service unit templates bundled with the CLI. // -//go:embed agent airgap-agent airgap-controller container-agent container-controller controller +//go:embed controller container-controller airgap-controller edgelet var FS embed.FS From 47a698ed72f830620b0a478d77069947cdbd23d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 23:47:27 +0300 Subject: [PATCH 20/63] Add Docker container engine client and edgelet binary download helpers. Introduce pkg/containerengine for local container operations and pkg/util helpers for GitHub release downloads and network checks. --- pkg/containerengine/docker/client.go | 44 ++++ pkg/containerengine/docker/container.go | 292 ++++++++++++++++++++++++ pkg/containerengine/docker/copy.go | 91 ++++++++ pkg/containerengine/docker/image.go | 90 ++++++++ pkg/util/edgelet_binary.go | 138 +++++++++++ pkg/util/edgelet_binary_test.go | 108 +++++++++ pkg/util/edgelet_binary_testutil.go | 21 ++ pkg/util/net.go | 50 ++++ pkg/util/net_test.go | 33 +++ 9 files changed, 867 insertions(+) create mode 100644 pkg/containerengine/docker/client.go create mode 100644 pkg/containerengine/docker/container.go create mode 100644 pkg/containerengine/docker/copy.go create mode 100644 pkg/containerengine/docker/image.go create mode 100644 pkg/util/edgelet_binary.go create mode 100644 pkg/util/edgelet_binary_test.go create mode 100644 pkg/util/edgelet_binary_testutil.go create mode 100644 pkg/util/net.go create mode 100644 pkg/util/net_test.go diff --git a/pkg/containerengine/docker/client.go b/pkg/containerengine/docker/client.go new file mode 100644 index 000000000..75579f532 --- /dev/null +++ b/pkg/containerengine/docker/client.go @@ -0,0 +1,44 @@ +// Package docker wraps the Moby API for legacy local control plane 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..fd2fc1c84 --- /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 && 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..76396e18c --- /dev/null +++ b/pkg/containerengine/docker/copy.go @@ -0,0 +1,91 @@ +package docker + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "io" + "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 { + zr := gzip.NewWriter(buf) + tw := tar.NewWriter(zr) + + srcLength := len(filepath.ToSlash(src)) + err := filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if file == src { + return nil + } + header, err := tar.FileInfoHeader(fi, file) + if err != nil { + return err + } + name := string([]rune(filepath.ToSlash(file))[srcLength:]) + header.Name = name + if err := tw.WriteHeader(header); err != nil { + return err + } + if !fi.IsDir() { + data, err := os.Open(file) + if err != nil { + return err + } + defer data.Close() + if _, err := io.Copy(tw, data); 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..a5fdad1ce --- /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) + 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/util/edgelet_binary.go b/pkg/util/edgelet_binary.go new file mode 100644 index 000000000..4d292bafd --- /dev/null +++ b/pkg/util/edgelet_binary.go @@ -0,0 +1,138 @@ +package util + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" +) + +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 +} + +// DownloadEdgeletBinary fetches the release binary for os/arch into destPath. +func DownloadEdgeletBinary(osName, archName, destPath string) error { + url, err := EdgeletBinaryURL(osName, archName) + if err != nil { + return err + } + + resp, err := http.Get(url) + 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, url) + } + + if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { + return fmt.Errorf("create download dir: %w", err) + } + + out, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) + if err != nil { + return fmt.Errorf("create edgelet binary file: %w", err) + } + defer out.Close() + + 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..d9176d665 --- /dev/null +++ b/pkg/util/edgelet_binary_test.go @@ -0,0 +1,108 @@ +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) { + edgeletReleaseBase = "https://github.com/Datasance/edgelet/releases/download" + edgeletBinaryVersion = "v1.0.0-rc.3" + + 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.3/edgelet-linux-amd64" + if got != want { + t.Fatalf("EdgeletBinaryURL = %q, want %q", got, want) + } +} + +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 TestDownloadEdgeletBinary(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1.0.0-rc.3/edgelet-linux-amd64" { + http.NotFound(w, r) + return + } + _, _ = w.Write([]byte("edgelet-binary-bytes")) + })) + defer server.Close() + + edgeletReleaseBase = server.URL + edgeletBinaryVersion = "v1.0.0-rc.3" + + 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/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) + } +} From d8e46657b2a6189fdb63a70fc49ffb3ec3beac74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 23:47:33 +0300 Subject: [PATCH 21/63] Implement edgelet install procedures and remove legacy Java agent installers. Add local and remote edgelet config, script orchestration, and container engine wiring while deleting local_agent and remote_agent install paths. --- pkg/iofog/install/agent.go | 6 + pkg/iofog/install/container_engine.go | 42 ++ pkg/iofog/install/container_engine_test.go | 35 + pkg/iofog/install/edgelet_config.go | 609 ++++++++++++++++++ pkg/iofog/install/edgelet_config_test.go | 342 ++++++++++ pkg/iofog/install/edgelet_local.go | 280 ++++++++ pkg/iofog/install/edgelet_local_test.go | 47 ++ pkg/iofog/install/edgelet_procedures_test.go | 190 ++++++ pkg/iofog/install/edgelet_remote.go | 426 ++++++++++++ pkg/iofog/install/edgelet_remote_test.go | 103 +++ pkg/iofog/install/edgelet_scripts.go | 384 +++++++++++ pkg/iofog/install/k8s.go | 9 +- pkg/iofog/install/k8s_controller_pods_test.go | 57 ++ pkg/iofog/install/local_agent.go | 75 --- pkg/iofog/install/local_container.go | 506 ++------------- pkg/iofog/install/paths.go | 29 + pkg/iofog/install/pkg.go | 50 +- pkg/iofog/install/procedures.go | 50 ++ pkg/iofog/install/procedures_test.go | 15 + pkg/iofog/install/remote_agent.go | 607 ----------------- pkg/iofog/install/remote_agent_test.go | 162 ----- pkg/iofog/install/test_helpers_test.go | 38 ++ 22 files changed, 2761 insertions(+), 1301 deletions(-) create mode 100644 pkg/iofog/install/container_engine.go create mode 100644 pkg/iofog/install/container_engine_test.go create mode 100644 pkg/iofog/install/edgelet_config.go create mode 100644 pkg/iofog/install/edgelet_config_test.go create mode 100644 pkg/iofog/install/edgelet_local.go create mode 100644 pkg/iofog/install/edgelet_local_test.go create mode 100644 pkg/iofog/install/edgelet_procedures_test.go create mode 100644 pkg/iofog/install/edgelet_remote.go create mode 100644 pkg/iofog/install/edgelet_remote_test.go create mode 100644 pkg/iofog/install/edgelet_scripts.go create mode 100644 pkg/iofog/install/k8s_controller_pods_test.go delete mode 100644 pkg/iofog/install/local_agent.go create mode 100644 pkg/iofog/install/paths.go create mode 100644 pkg/iofog/install/procedures.go create mode 100644 pkg/iofog/install/procedures_test.go delete mode 100644 pkg/iofog/install/remote_agent.go delete mode 100644 pkg/iofog/install/remote_agent_test.go create mode 100644 pkg/iofog/install/test_helpers_test.go diff --git a/pkg/iofog/install/agent.go b/pkg/iofog/install/agent.go index 27d436e22..7b28ed0ed 100644 --- a/pkg/iofog/install/agent.go +++ b/pkg/iofog/install/agent.go @@ -10,6 +10,9 @@ type Agent interface { getProvisionKey(string, IofogUser) (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) (string, string, error) + // defaultAgent implements commong behavior type defaultAgent struct { name string @@ -17,6 +20,9 @@ type defaultAgent struct { } func (agent *defaultAgent) getProvisionKey(controllerEndpoint string, user IofogUser) (key string, caCert string, err error) { + if getProvisionKeyHook != nil { + return getProvisionKeyHook(agent, controllerEndpoint, user) + } // Connect to controller baseURL, err := util.GetBaseURL(controllerEndpoint) if err != nil { diff --git a/pkg/iofog/install/container_engine.go b/pkg/iofog/install/container_engine.go new file mode 100644 index 000000000..8250063cc --- /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 legacy LocalController docker deploy. +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/edgelet_config.go b/pkg/iofog/install/edgelet_config.go new file mode 100644 index 000000000..badeda932 --- /dev/null +++ b/pkg/iofog/install/edgelet_config.go @@ -0,0 +1,609 @@ +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, 0o755) + } + 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), 0o755); 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 { + tmp.Close() + 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. +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..b5457014f --- /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/potctl-edgelet-scripts/bin:") { + t.Fatalf("expected stage bin on PATH in bootstrap env, got %q", env) + } +} diff --git a/pkg/iofog/install/edgelet_local.go b/pkg/iofog/install/edgelet_local.go new file mode 100644 index 000000000..68f4aa371 --- /dev/null +++ b/pkg/iofog/install/edgelet_local.go @@ -0,0 +1,280 @@ +package install + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "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 := os.ReadFile(filepath.Join(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 " + cmd + } + return agent.runShell(agent.cfg.bootstrapEnv(true) + " " + cmd) +} + +func (agent *LocalEdgelet) edgeletCommand(cmd, _ string) string { + prefix := "" + if needsLocalSudo(agent.cfg) { + prefix = "sudo " + } + return agent.cfg.bootstrapEnv(true) + " " + prefix + cmd +} + +func (agent *LocalEdgelet) Configure(controllerEndpoint string, user IofogUser) (string, error) { + key, caCert, err := agent.getProvisionKey(controllerEndpoint, user) + 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, 0o755); err != nil { + return err + } + if err := os.MkdirAll(filepath.Join(agent.dir, "lib"), 0o755); err != nil { + return err + } + for idx, script := range agent.procs.scriptNames { + path := filepath.Join(agent.dir, script) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + if err := os.WriteFile(path, []byte(agent.procs.scriptContents[idx]), 0o755); err != nil { + 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..d7ac2e9ad --- /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.3") + 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.3") { + 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.3") + + 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..4f9d01a1c --- /dev/null +++ b/pkg/iofog/install/edgelet_remote.go @@ -0,0 +1,426 @@ +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" +) + +// 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 := os.ReadFile(filepath.Join(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) (string, error) { + key, caCert, err := agent.getProvisionKey(controllerEndpoint, user) + 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 +} + +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) 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 "potctl-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..13e945be6 --- /dev/null +++ b/pkg/iofog/install/edgelet_remote_test.go @@ -0,0 +1,103 @@ +package install + +import ( + "strings" + "testing" + + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +func TestRemoteEdgeletBootstrapUsesMockedSSH(t *testing.T) { + util.SetEdgeletReleaseBaseForTest("https://example.com/download") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.3") + 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) (string, string, error) { + return "provision-key", "base64-ca", nil + } + t.Cleanup(func() { getProvisionKeyHook = nil }) + + if _, err := agent.Configure("https://controller.example.com", IofogUser{}); 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..804a81e2f --- /dev/null +++ b/pkg/iofog/install/edgelet_scripts.go @@ -0,0 +1,384 @@ +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(err.Error(), "not provisioned") +} diff --git a/pkg/iofog/install/k8s.go b/pkg/iofog/install/k8s.go index 4ad81d4d8..3748053b4 100644 --- a/pkg/iofog/install/k8s.go +++ b/pkg/iofog/install/k8s.go @@ -710,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), @@ -721,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/local_agent.go b/pkg/iofog/install/local_agent.go deleted file mode 100644 index f327f4bb1..000000000 --- a/pkg/iofog/install/local_agent.go +++ /dev/null @@ -1,75 +0,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 ac1f5a946..5359bee41 100644 --- a/pkg/iofog/install/local_container.go +++ b/pkg/iofog/install/local_container.go @@ -1,35 +1,20 @@ 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/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" "github.com/eclipse-iofog/iofogctl/pkg/iofog" "github.com/eclipse-iofog/iofogctl/pkg/util" ) // 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 @@ -77,24 +62,11 @@ type LocalContainerPort struct { 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 { +func GetLocalContainerName(kind string, _ bool) string { + if kind != "controller" { return "" } - if isSystem { - return name + "-system" - } - return name + return sanitizeContainerName("iofog-controller") } func sanitizeContainerName(name string) string { @@ -102,48 +74,6 @@ func sanitizeContainerName(name string) string { 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 @@ -157,10 +87,9 @@ func NewLocalControllerConfig(image string, credentials Credentials, auth Auth, image = util.GetControllerImage() } - // Handle nil pointer fields safely sslValue := "false" if db.SSL != nil { - sslValue = strconv.FormatBool(*db.SSL) + sslValue = fmt.Sprintf("%t", *db.SSL) } caValue := "" @@ -174,19 +103,15 @@ func NewLocalControllerConfig(image string, credentials Credentials, auth Auth, "DB_HOST=" + db.Host, "DB_USERNAME=" + db.User, "DB_PASSWORD=" + db.Password, - "DB_PORT=" + strconv.Itoa(db.Port), + "DB_PORT=" + fmt.Sprintf("%d", db.Port), "DB_NAME=" + db.DatabaseName, "DB_USE_SSL=" + sslValue, "DB_SSL_CA=" + caValue, } - _ = auth // v3.8 auth is embedded/external OIDC — not Keycloak env vars + _ = auth - // 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)) @@ -194,7 +119,6 @@ func NewLocalControllerConfig(image string, credentials Credentials, auth Auth, 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)) } @@ -238,229 +162,70 @@ func NewLocalControllerConfig(image string, credentials Credentials, auth Auth, } } -// 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{Signal: "SIGTERM", Timeout: 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{Signal: "SIGTERM", Timeout: 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 + return lc.client.RemoveContainerByID(id) } -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) -} - -// 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 - } + Privileged: containerConfig.Privileged, + NetworkMode: containerConfig.NetworkMode, + PortBindings: portBindings, + RestartPolicy: dockengine.RestartPolicyAlways, + StopTimeout: 60 * time.Second, } - - 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)) + pullOpts := dockengine.PullOptions{ + Username: containerConfig.Credentials.User, + Password: containerConfig.Credentials.Password, } - - // 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)) - } - - return container.ID, err + return lc.client.DeployContainer(opts, pullOpts) } // Returns endpoint to reach controller container from within another container @@ -474,179 +239,42 @@ func (lc *LocalContainer) GetLocalControllerEndpoint() (controllerEndpoint strin } 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) + result, err := lc.client.ExecuteCmd(name, cmd) 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) - 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..7f1e7fcaa --- /dev/null +++ b/pkg/iofog/install/paths.go @@ -0,0 +1,29 @@ +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/potctl-edgelet-scripts" + +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 4da0a349d..58b94d58e 100644 --- a/pkg/iofog/install/pkg.go +++ b/pkg/iofog/install/pkg.go @@ -1,13 +1,19 @@ package install var pkg struct { - scriptPrereq string - scriptInit string - scriptInstallDeps string - scriptInstallJava string - scriptInstallContainerEngine string - scriptInstallIofog string - scriptUninstallIofog 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 controllerScriptPrereq string controllerScriptInit string controllerScriptInstallContainerEngine string @@ -15,18 +21,31 @@ var pkg struct { controllerScriptInstall string controllerScriptUninstall 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.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.controllerScriptPrereq = "check_prereqs.sh" pkg.controllerScriptInit = "init.sh" pkg.controllerScriptInstallContainerEngine = "install_container_engine.sh" @@ -34,6 +53,5 @@ func init() { pkg.controllerScriptInstall = "install_iofog.sh" pkg.controllerScriptUninstall = "uninstall_iofog.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..1699efa6c --- /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/potctl-edgelet-scripts/install.sh", + Args: []string{"--version=v1.0.0-rc.3", "--arch=arm64", "--skip-start"}, + } + got := ep.getCommand() + want := "/tmp/potctl-edgelet-scripts/install.sh '--version=v1.0.0-rc.3' '--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 53b12da5b..000000000 --- a/pkg/iofog/install/remote_agent.go +++ /dev/null @@ -1,607 +0,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 dependencies 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, arch string, - // latitude, longitude float64, - // description, arch string, - agentConfig client.AgentConfiguration, -) error { - // Prepare the base commands for agent configuration - cmds := []command{} - - // Convert Arch (string) to required format if necessary - archValue := arch - if arch == "" { - archValue = "auto" // Default value if arch 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": archValue, - // "-gps": gpsCoordinates, - } - - // Add values from agentConfig to configOptions, properly handling pointers - if agentConfig.NetworkInterface != nil && *agentConfig.NetworkInterface != "" { - configOptions["-n"] = *agentConfig.NetworkInterface - } - if agentConfig.ContainerEngineURL != nil && *agentConfig.ContainerEngineURL != "" { - configOptions["-c"] = *agentConfig.ContainerEngineURL - } - 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.PruningFrequency != nil { - configOptions["-pf"] = strconv.FormatFloat(*agentConfig.PruningFrequency, '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 1b00121cd..000000000 --- a/pkg/iofog/install/remote_agent_test.go +++ /dev/null @@ -1,162 +0,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) +} From 085940ed9c8efde377feb78190a261262a8cb0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 23:47:39 +0300 Subject: [PATCH 22/63] Extend agent resource model for edgelet deployments and SDK arch mapping. Add runtime status fields, package registry YAML, edgelet golden tests, and align control plane fixture images with controller 3.8.0-rc.2. --- internal/resource/edgelet_golden_test.go | 162 ++++++++++++++++++ internal/resource/local_agent.go | 19 +- .../testdata/edgelet/agent-config-spec.yaml | 42 +++++ .../testdata/edgelet/local-agent-spec.yaml | 49 ++++++ .../remote-agent-package-registry.yaml | 27 +++ .../testdata/edgelet/remote-agent-spec.yaml | 50 ++++++ .../testdata/k8s/controlplane-datasance.yaml | 2 +- .../testdata/k8s/controlplane-iofog.yaml | 2 +- .../resource/testdata/k8s/controlplane.yaml | 2 +- internal/resource/types.go | 37 ++-- internal/resource/yaml.go | 8 + 11 files changed, 377 insertions(+), 23 deletions(-) create mode 100644 internal/resource/edgelet_golden_test.go create mode 100644 internal/resource/testdata/edgelet/agent-config-spec.yaml create mode 100644 internal/resource/testdata/edgelet/local-agent-spec.yaml create mode 100644 internal/resource/testdata/edgelet/remote-agent-package-registry.yaml create mode 100644 internal/resource/testdata/edgelet/remote-agent-spec.yaml diff --git a/internal/resource/edgelet_golden_test.go b/internal/resource/edgelet_golden_test.go new file mode 100644 index 000000000..c884f6a99 --- /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.RouterConfig.RouterMode) + require.Equal(t, "edge", *cfg.RouterConfig.RouterMode) + require.NotNil(t, cfg.RouterConfig.MessagingPort) + require.Equal(t, 5671, *cfg.RouterConfig.MessagingPort) + require.NotNil(t, cfg.NatsConfig.NatsMode) + require.Equal(t, "leaf", *cfg.NatsConfig.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.3", agent.Package.Version) + require.Equal(t, "ghcr.io/datasance/edgelet:1.0.0-rc.3", 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.3"}, 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.3", agent.Package.Version) + require.Equal(t, "ghcr.io/datasance/edgelet:1.0.0-rc.3", 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.RouterConfig.RouterMode) + require.Equal(t, "edge", *cfg.RouterConfig.RouterMode) + require.NotNil(t, cfg.NatsConfig.NatsMode) + require.Equal(t, "leaf", *cfg.NatsConfig.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/local_agent.go b/internal/resource/local_agent.go index eb06b0135..8935c6914 100644 --- a/internal/resource/local_agent.go +++ b/internal/resource/local_agent.go @@ -3,11 +3,13 @@ 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 { @@ -19,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" } @@ -67,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/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..cafbbc1d4 --- /dev/null +++ b/internal/resource/testdata/edgelet/local-agent-spec.yaml @@ -0,0 +1,49 @@ +package: + version: 1.0.0-rc.3 + container: + image: ghcr.io/datasance/edgelet:1.0.0-rc.3 +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..b3e6b0c0a --- /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.3 + container: + image: ghcr.io/datasance/edgelet:1.0.0-rc.3 + 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.3 + 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 index fc53dc2c9..e2a2ead7b 100644 --- a/internal/resource/testdata/k8s/controlplane-datasance.yaml +++ b/internal/resource/testdata/k8s/controlplane-datasance.yaml @@ -26,7 +26,7 @@ events: captureIpAddress: true images: operator: ghcr.io/datasance/operator:3.8.0-rc.1 - controller: ghcr.io/datasance/controller:3.8.0-rc.1 + controller: ghcr.io/datasance/controller:3.8.0-rc.2 router: ghcr.io/datasance/router:3.8.0-rc.1 nats: ghcr.io/datasance/nats:2.14.2-rc.1 nats: diff --git a/internal/resource/testdata/k8s/controlplane-iofog.yaml b/internal/resource/testdata/k8s/controlplane-iofog.yaml index a06d45a66..5ddd6d016 100644 --- a/internal/resource/testdata/k8s/controlplane-iofog.yaml +++ b/internal/resource/testdata/k8s/controlplane-iofog.yaml @@ -26,7 +26,7 @@ events: captureIpAddress: true images: operator: ghcr.io/eclipse-iofog/operator:3.8.0-rc.1 - controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.1 + controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.2 router: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 nats: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 nats: diff --git a/internal/resource/testdata/k8s/controlplane.yaml b/internal/resource/testdata/k8s/controlplane.yaml index cd23b041f..bae157d14 100644 --- a/internal/resource/testdata/k8s/controlplane.yaml +++ b/internal/resource/testdata/k8s/controlplane.yaml @@ -53,7 +53,7 @@ spec: # id: controller # secret: "" images: - controller: ghcr.io/datasance/controller:3.8.0-rc.1 + controller: ghcr.io/datasance/controller:3.8.0-rc.2 router: ghcr.io/datasance/router:3.8.0-rc.1 nats: ghcr.io/datasance/nats:2.14.2-rc.1 nats: diff --git a/internal/resource/types.go b/internal/resource/types.go index f30f226bd..3c8ed0531 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -4,6 +4,7 @@ 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" ) @@ -17,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 { @@ -213,7 +213,10 @@ type AgentStatus struct { IsReadyToRollback bool `json:"isReadyToRollback" yaml:"isReadyToRollback"` Tunnel string `json:"tunnel" yaml:"tunnel"` VolumeMounts []VolumeMount - GpsStatus string `json:"gpsStatus" yaml:"gpsStatus"` + GpsStatus string `json:"gpsStatus" yaml:"gpsStatus"` + AvailableRuntimes []string `json:"availableRuntimes" yaml:"availableRuntimes"` + RuntimeAgentPhase string `json:"runtimeAgentPhase" yaml:"runtimeAgentPhase"` + ControlPlaneQuiesced bool `json:"controlPlaneQuiesced" yaml:"controlPlaneQuiesced"` } type EdgeResource struct { @@ -232,18 +235,16 @@ type EdgeResourceHTTPInterface = client.HTTPEdgeResource type Display = client.EdgeResourceDisplay type HTTPEndpoint = client.HTTPEndpoint -// ArchStringMap map human readable fog type to Controller fog type -var ArchStringMap = 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 } -// ArchIntMap map Controller fog type to human readable fog type -var ArchIntMap = 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 } // ControllerConfig is operator-aligned runtime config for the ioFog Controller (spec.controller). diff --git a/internal/resource/yaml.go b/internal/resource/yaml.go index 5a40d78b4..4247cb3f2 100644 --- a/internal/resource/yaml.go +++ b/internal/resource/yaml.go @@ -103,3 +103,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 +} From ed3264c958bc269b40199d43e873ea7c486f34bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 23:47:45 +0300 Subject: [PATCH 23/63] Wire edgelet into local, remote, and airgap deploy flows. Add edgelet deploy executors, airgap binary transfer, control plane airgap propagation, and offline image updates for the new installation model. --- internal/config/config.go | 33 +- internal/deploy/agent/edgelet.go | 74 +++++ internal/deploy/agent/factory.go | 17 +- internal/deploy/agent/local.go | 106 +++---- internal/deploy/agent/remote.go | 282 +++++------------- internal/deploy/agentconfig/utils.go | 2 +- internal/deploy/airgap/agent.go | 62 ++++ internal/deploy/airgap/agent_edgelet.go | 101 +++++++ internal/deploy/airgap/binary.go | 177 +++++++++++ internal/deploy/airgap/binary_test.go | 73 +++++ internal/deploy/airgap/cache.go | 4 +- internal/deploy/airgap/helpers.go | 162 +++++++++- internal/deploy/airgap/helpers_test.go | 223 ++++++++++++++ internal/deploy/airgap/transfer.go | 20 +- internal/deploy/controller/local/local.go | 2 +- .../k8s/testdata/cp-cr-datasance.yaml | 2 +- .../k8s/testdata/cp-cr-iofog.yaml | 2 +- .../deploy/controlplane/k8s/translate_test.go | 12 +- .../deploy/controlplane/remote/execute.go | 142 ++------- internal/deploy/offlineimage/images.go | 10 +- internal/deploy/offlineimage/progress.go | 2 +- 21 files changed, 1054 insertions(+), 454 deletions(-) create mode 100644 internal/deploy/agent/edgelet.go create mode 100644 internal/deploy/airgap/agent.go create mode 100644 internal/deploy/airgap/agent_edgelet.go create mode 100644 internal/deploy/airgap/binary.go create mode 100644 internal/deploy/airgap/binary_test.go create mode 100644 internal/deploy/airgap/helpers_test.go diff --git a/internal/config/config.go b/internal/config/config.go index fc4c6dc6c..972a2fad1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,15 +27,16 @@ var ( ) const ( - latestVersion = "v3" - 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() { @@ -239,6 +240,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 { diff --git a/internal/deploy/agent/edgelet.go b/internal/deploy/agent/edgelet.go new file mode 100644 index 000000000..f2d6bc614 --- /dev/null +++ b/internal/deploy/agent/edgelet.go @@ -0,0 +1,74 @@ +package deployagent + +import ( + "strings" + + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const localAgentConflictPort = 54321 + +type edgeletAgent interface { + Bootstrap() error + Configure(controllerEndpoint string, user install.IofogUser) (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 { + 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 +} + +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/factory.go b/internal/deploy/agent/factory.go index 34a79b98d..199ca99b2 100644 --- a/internal/deploy/agent/factory.go +++ b/internal/deploy/agent/factory.go @@ -61,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 { @@ -75,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 6c7b5fad4..beb7e0139 100644 --- a/internal/deploy/agent/local.go +++ b/internal/deploy/agent/local.go @@ -1,57 +1,55 @@ package deployagent import ( - "fmt" - "regexp" - "github.com/eclipse-iofog/iofogctl/internal/config" + 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" ) 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 @@ -60,7 +58,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() @@ -69,10 +67,9 @@ 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) + return edgelet.Configure(controllerEndpoint, user) } func (exe *localExecutor) GetName() string { @@ -80,51 +77,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 6ae4ee04d..b6c242f5b 100644 --- a/internal/deploy/agent/remote.go +++ b/internal/deploy/agent/remote.go @@ -7,9 +7,6 @@ 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" "github.com/eclipse-iofog/iofogctl/pkg/iofog" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" @@ -18,58 +15,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 } @@ -82,7 +67,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() @@ -91,43 +76,12 @@ 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 arch *string - if agentConfig.Arch == nil { - auto := "auto" - arch = &auto - } else { - arch = agentConfig.Arch - } - err = agent.SetInitialConfig( - agentConfig.Name, - // agentConfig.Location, - // agentConfig.Latitude, - // agentConfig.Longitude, - // agentConfig.Description, - *arch, - agentConfig.AgentConfiguration, // Pass the embedded client.AgentConfiguration - ) - if err != nil { - return "", err - } - } - return agent.Configure(controllerEndpoint, user) + return edgelet.Configure(controllerEndpoint, user) } -// 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 @@ -135,174 +89,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.Arch) - if err != nil { - return fmt.Errorf("failed to resolve platform: %w", err) - } + var pendingNativeAirgap *deployairgap.AgentAirgapResult - engine, err := deployairgap.ResolveContainerEngine(exe.agent.Config.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.NatsAMD64 != "" { - imageList = append(imageList, images.NatsAMD64) - } - if images.DebuggerAMD64 != "" { - imageList = append(imageList, images.DebuggerAMD64) - } - if images.NatsARM64 != "" { - imageList = append(imageList, images.NatsARM64) - } - if images.DebuggerARM64 != "" { - imageList = append(imageList, images.DebuggerARM64) - } - if images.NatsRISCV64 != "" { - imageList = append(imageList, images.NatsRISCV64) - } - if images.DebuggerRISCV64 != "" { - imageList = append(imageList, images.DebuggerRISCV64) - } - if images.NatsARM != "" { - imageList = append(imageList, images.NatsARM) - } - if images.DebuggerARM != "" { - imageList = append(imageList, images.DebuggerARM) - } + 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/utils.go b/internal/deploy/agentconfig/utils.go index bf776447a..cd70be280 100644 --- a/internal/deploy/agentconfig/utils.go +++ b/internal/deploy/agentconfig/utils.go @@ -127,7 +127,7 @@ func Process(agentConfig *rsc.AgentConfiguration, name, agentIP string, otherAge func getAgentUpdateRequestFromAgentConfig(agentConfig *rsc.AgentConfiguration, tags *[]string) (request client.AgentUpdateRequest) { var archPtr *int64 if agentConfig.Arch != nil { - arch, found := rsc.ArchStringMap[*agentConfig.Arch] + arch, found := rsc.ArchStringToID(*agentConfig.Arch) if !found { arch = 0 } 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..e0ada3eef --- /dev/null +++ b/internal/deploy/airgap/agent_edgelet.go @@ -0,0 +1,101 @@ +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 := false + if cfg.DeploymentType != nil && strings.EqualFold(strings.TrimSpace(*cfg.DeploymentType), DeploymentTypeContainer) { + useContainer = true + } + 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..856417246 --- /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, 0o755); 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 := os.ReadFile(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 os.WriteFile(path, data, 0o644) +} + +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 := os.Open(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..88778b19e --- /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.3/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.3") + 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..361024a6e 100644 --- a/internal/deploy/airgap/cache.go +++ b/internal/deploy/airgap/cache.go @@ -7,9 +7,9 @@ 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" ) const cacheMetadataFilename = "metadata.json" diff --git a/internal/deploy/airgap/helpers.go b/internal/deploy/airgap/helpers.go index c32743054..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" @@ -12,19 +13,29 @@ const ( 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) } +// 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") @@ -35,17 +46,38 @@ func ResolvePlatform(arch *string) (string, error) { 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 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": @@ -55,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" @@ -77,23 +208,36 @@ 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 Arch 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.ContainerEngine == nil || *agentConfig.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 @@ -105,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/transfer.go b/internal/deploy/airgap/transfer.go index d97e13c69..5dafd837c 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, } @@ -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/controller/local/local.go b/internal/deploy/controller/local/local.go index 7ba1b179f..1f4225fcf 100644 --- a/internal/deploy/controller/local/local.go +++ b/internal/deploy/controller/local/local.go @@ -74,7 +74,7 @@ func NewExecutorWithoutParsing(namespace string, controlPlane *rsc.LocalControlP if err := util.IsLowerAlphanumeric("Controller", controller.GetName()); err != nil { return nil, err } - cli, err := install.NewLocalContainerClient() + cli, err := install.NewLocalContainerClient(install.DefaultLocalContainerEngine, nil) if err != nil { return nil, err } diff --git a/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml b/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml index f3c7c9d72..3205cad42 100644 --- a/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml +++ b/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml @@ -25,7 +25,7 @@ spec: cleanupInterval: 86400 captureIpAddress: true images: - controller: ghcr.io/datasance/controller:3.8.0-rc.1 + controller: ghcr.io/datasance/controller:3.8.0-rc.2 router: ghcr.io/datasance/router:3.8.0-rc.1 nats: ghcr.io/datasance/nats:2.14.2-rc.1 nats: diff --git a/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml b/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml index 5574809b9..5a9f10359 100644 --- a/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml +++ b/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml @@ -25,7 +25,7 @@ spec: cleanupInterval: 86400 captureIpAddress: true images: - controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.1 + controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.2 router: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 nats: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 nats: diff --git a/internal/deploy/controlplane/k8s/translate_test.go b/internal/deploy/controlplane/k8s/translate_test.go index 81c503150..3f4f97264 100644 --- a/internal/deploy/controlplane/k8s/translate_test.go +++ b/internal/deploy/controlplane/k8s/translate_test.go @@ -64,7 +64,7 @@ func TestTranslateToControlPlaneCR_DatasanceGolden(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "datasance.com/v3", crName: "pot", - controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.1", + controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.2", routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", natsImage: "ghcr.io/datasance/nats:2.14.2-rc.1", }) @@ -77,7 +77,7 @@ func TestTranslateToControlPlaneCR_IofogGolden(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "iofog.org/v3", crName: "iofog", - controllerImage: "ghcr.io/eclipse-iofog/controller:3.8.0-rc.1", + controllerImage: "ghcr.io/eclipse-iofog/controller:3.8.0-rc.2", routerImage: "ghcr.io/eclipse-iofog/router:3.8.0-rc.1", natsImage: "ghcr.io/eclipse-iofog/nats:2.14.2-rc.1", }) @@ -91,12 +91,12 @@ func TestTranslateToControlPlaneCR_StripsOperatorImage(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "datasance.com/v3", crName: "pot", - controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.1", + controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.2", routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", natsImage: "ghcr.io/datasance/nats:2.14.2-rc.1", }) require.NotContains(t, got.Spec.Images.Controller, "operator") - require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.1", got.Spec.Images.Controller) + require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.2", got.Spec.Images.Controller) } func TestTranslateToControlPlaneCR_DefaultImagesWhenOmitted(t *testing.T) { @@ -107,11 +107,11 @@ func TestTranslateToControlPlaneCR_DefaultImagesWhenOmitted(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "datasance.com/v3", crName: "pot", - controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.1", + controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.2", routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", natsImage: "ghcr.io/datasance/nats:2.14.2-rc.1", }) - require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.1", got.Spec.Images.Controller) + require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.2", 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.1", got.Spec.Images.Nats) } diff --git a/internal/deploy/controlplane/remote/execute.go b/internal/deploy/controlplane/remote/execute.go index e3c4cc3ce..1be038e52 100644 --- a/internal/deploy/controlplane/remote/execute.go +++ b/internal/deploy/controlplane/remote/execute.go @@ -82,6 +82,23 @@ type remoteControlPlaneExecutor struct { name string } +func applyControlPlaneAirgapFlag(namespace string, agent *rsc.RemoteAgent) { + if agent == nil { + return + } + ns, err := config.GetNamespace(namespace) + if err != nil { + return + } + cp, err := ns.GetControlPlane() + if err != nil { + return + } + if remoteCP, ok := cp.(*rsc.RemoteControlPlane); ok { + agent.Airgap = remoteCP.Airgap + } +} + 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) @@ -182,6 +199,7 @@ func deploySystemAgent(namespace string, ctrl *rsc.RemoteController, systemAgent agent.Package = systemAgentConfig.Package agent.Scripts = systemAgentConfig.Scripts // Support custom scripts } + applyControlPlaneAirgapFlag(namespace, &agent) // Get Agentconfig executor deployAgentConfigExecutor := deployagentconfig.NewRemoteExecutor(ctrl.Name, &deployAgentConfig, namespace, nil) @@ -328,15 +346,7 @@ func deployNextSystemAgent(namespace string, ctrl *rsc.RemoteController, systemA 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 - } - } - } + applyControlPlaneAirgapFlag(namespace, &agent) // Get Agentconfig executor deployAgentConfigExecutor := deployagentconfig.NewRemoteExecutor(ctrl.Name, &deployAgentConfig, namespace, nil) @@ -432,14 +442,6 @@ func (exe remoteControlPlaneExecutor) postDeploy() (err error) { return util.NewInternalError("Could not convert ControlPlane to Remote ControlPlane") } - // 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) - } - } - // Deploy agents for each controller for idx, baseController := range controllers { controller, ok := baseController.(*rsc.RemoteController) @@ -766,11 +768,11 @@ func (exe remoteControlPlaneExecutor) transferControllerImages() error { if err != nil { return fmt.Errorf("controller %s: %w", controller.Name, err) } - engine, err := deployairgap.ResolveContainerEngine(controller.SystemAgent.AgentConfiguration.ContainerEngine) + opts, err := deployairgap.ControllerAirgapLoadOptions(controller.SystemAgent.AgentConfiguration) 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 { + if err := deployairgap.TransferAirgapImages(ctx, exe.ns.Name, controller.Host, &controller.SSH, platform, opts, imageList); err != nil { return fmt.Errorf("failed to transfer images to controller %s: %w", controller.Name, err) } } @@ -778,108 +780,6 @@ func (exe remoteControlPlaneExecutor) transferControllerImages() error { return 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") - } - - // 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) - } - - 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.Arch) - if err != nil { - return fmt.Errorf("system agent for controller %s: %w", controller.Name, err) - } - - engine, err := deployairgap.ResolveContainerEngine(controller.SystemAgent.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.NatsAMD64 != "" { - imageList = append(imageList, images.NatsAMD64) - } - if images.NatsARM64 != "" { - imageList = append(imageList, images.NatsARM64) - } - if images.NatsRISCV64 != "" { - imageList = append(imageList, images.NatsRISCV64) - } - if images.NatsARM != "" { - imageList = append(imageList, images.NatsARM) - } - if images.DebuggerAMD64 != "" { - imageList = append(imageList, images.DebuggerAMD64) - } - if images.DebuggerARM64 != "" { - imageList = append(imageList, images.DebuggerARM64) - } - if images.DebuggerRISCV64 != "" { - imageList = append(imageList, images.DebuggerRISCV64) - } - if images.DebuggerARM != "" { - imageList = append(imageList, images.DebuggerARM) - } - - // 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) - } - } - - return nil -} - func NewExecutor(opt Options) (exe execute.Executor, err error) { // Check the namespace exists ns, err := config.GetNamespace(opt.Namespace) diff --git a/internal/deploy/offlineimage/images.go b/internal/deploy/offlineimage/images.go index f347c131f..d61885385 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 { 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 { From 8f13bc11d3ea1efb4e0de1b25794ee1c497636ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 22 Jun 2026 23:47:58 +0300 Subject: [PATCH 24/63] Update agent lifecycle commands, observability, and legacy CLI for edgelet. Retarget delete, detach, prune, and logs to edgelet paths, extend describe status fields, and replace iofog-agent legacy invocations with edgelet. --- internal/cmd/legacy.go | 9 +- internal/delete/agent/execute.go | 4 +- internal/delete/agent/local.go | 22 +++-- internal/delete/agent/remote.go | 33 ++++--- internal/delete/controller/local.go | 2 +- internal/describe/agent_status_test.go | 92 +++++++++++++++++++ .../testdata/agent-config-status-v38.yaml | 25 +++++ internal/describe/utils.go | 3 + internal/detach/agent/execute.go | 4 +- internal/detach/agent/local.go | 19 ++-- internal/detach/agent/remote.go | 36 +++++--- internal/logs/agent.go | 16 +++- internal/logs/local_controller.go | 2 +- internal/logs/microservice.go | 5 +- internal/prune/agent/execute.go | 2 +- internal/prune/agent/local.go | 16 ++-- internal/prune/agent/remote.go | 10 +- internal/util/client/api.go | 5 +- internal/util/client/client.go | 46 +++++++--- internal/util/client/client_sync_test.go | 60 ++++++++++++ 20 files changed, 321 insertions(+), 90 deletions(-) create mode 100644 internal/describe/agent_status_test.go create mode 100644 internal/describe/testdata/agent-config-status-v38.yaml create mode 100644 internal/util/client/client_sync_test.go diff --git a/internal/cmd/legacy.go b/internal/cmd/legacy.go index 96dcabf35..51a6d618b 100644 --- a/internal/cmd/legacy.go +++ b/internal/cmd/legacy.go @@ -48,7 +48,7 @@ func k8sExecute(kubeConfig, namespace, podSelector string, cliCmd, cmd []string) func localExecute(container string, localCLI, localCmd []string) { // Execute command - localContainerClient, err := install.NewLocalContainerClient() + localContainerClient, err := install.NewLocalContainerClient(install.DefaultLocalContainerEngine, nil) util.Check(err) cmd := append(localCLI, localCmd...) result, err := localContainerClient.ExecuteCmd(container, cmd) @@ -139,14 +139,15 @@ iofogctl legacy agent NAME COMMAND`, util.Check(err) switch agent := baseAgent.(type) { case *rsc.LocalAgent: - localExecute(install.GetLocalContainerName("agent", false), []string{"iofog-agent"}, args[2:]) + out, err := util.Exec("", "edgelet", args[2:]...) + util.Check(err) + fmt.Print(out) 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:]) + remoteExec(agent.SSH.User, agent.Host, agent.SSH.KeyFile, agent.SSH.Port, "sudo edgelet", args[2:]) } default: util.Check(util.NewInputError("Unknown legacy CLI " + resource)) diff --git a/internal/delete/agent/execute.go b/internal/delete/agent/execute.go index c84ea3375..e49a4d9c3 100644 --- a/internal/delete/agent/execute.go +++ b/internal/delete/agent/execute.go @@ -69,8 +69,8 @@ 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())) + if err = exe.deleteLocalEdgelet(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: if err = exe.deleteRemoteAgent(agent); err != nil { diff --git a/internal/delete/agent/local.go b/internal/delete/agent/local.go index 5312966b1..338bc5131 100644 --- a/internal/delete/agent/local.go +++ b/internal/delete/agent/local.go @@ -4,23 +4,26 @@ 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) deleteLocalEdgelet(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 } - - // 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)) + 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 @@ -35,6 +38,5 @@ func (exe executor) deleteLocalContainer() error { } } } - return nil } diff --git a/internal/delete/agent/remote.go b/internal/delete/agent/remote.go index eaf30204f..9f20f2406 100644 --- a/internal/delete/agent/remote.go +++ b/internal/delete/agent/remote.go @@ -3,28 +3,33 @@ 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 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())) - } + 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.Uninstall(true); err != nil { + util.PrintNotify(fmt.Sprintf("Failed to stop daemon on Agent %s. %s", agent.Name, err.Error())) } return nil } diff --git a/internal/delete/controller/local.go b/internal/delete/controller/local.go index 8d45d29bd..f2224e805 100644 --- a/internal/delete/controller/local.go +++ b/internal/delete/controller/local.go @@ -35,7 +35,7 @@ func (exe *LocalExecutor) Execute() error { if err != nil { return err } - client, err := install.NewLocalContainerClient() + client, err := install.NewLocalContainerClient(install.DefaultLocalContainerEngine, nil) if err != nil { return err } diff --git a/internal/describe/agent_status_test.go b/internal/describe/agent_status_test.go new file mode 100644 index 000000000..8a451f752 --- /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 + ts := time.Date(2026, 5, 7, 15, 21, 10, 0, time.FixedZone("TRT", 3*3600)).UnixMilli() + status := rsc.AgentStatus{ + LastActive: ts, + LastStatusTimeMsUTC: ts, + 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/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 464cf84b3..c05e0a8b4 100644 --- a/internal/describe/utils.go +++ b/internal/describe/utils.go @@ -310,6 +310,9 @@ func FormatAgentStatus(status rsc.AgentStatus) map[string]interface{} { formatted["securityStatus"] = status.SecurityStatus formatted["warningMessage"] = status.WarningMessage formatted["securityViolationInfo"] = status.SecurityViolationInfo + formatted["availableRuntimes"] = status.AvailableRuntimes + formatted["runtimeAgentPhase"] = status.RuntimeAgentPhase + formatted["controlPlaneQuiesced"] = status.ControlPlaneQuiesced // Format timestamps if status.LastActive > 0 { diff --git a/internal/detach/agent/execute.go b/internal/detach/agent/execute.go index a62bc0679..818fc4a92 100644 --- a/internal/detach/agent/execute.go +++ b/internal/detach/agent/execute.go @@ -4,6 +4,7 @@ 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" @@ -72,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 f3decd6d8..0ae6bcb00 100644 --- a/internal/detach/agent/local.go +++ b/internal/detach/agent/local.go @@ -7,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 f54a2ee0c..ba14f6e50 100644 --- a/internal/detach/agent/remote.go +++ b/internal/detach/agent/remote.go @@ -3,6 +3,7 @@ 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" @@ -11,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/logs/agent.go b/internal/logs/agent.go index 83a9dd872..d95354987 100644 --- a/internal/logs/agent.go +++ b/internal/logs/agent.go @@ -5,6 +5,7 @@ import ( "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" @@ -47,14 +48,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 } @@ -131,3 +132,10 @@ func (exe *agentExecutor) Execute() error { 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/local_controller.go b/internal/logs/local_controller.go index 9034bc9bf..ea0251e76 100644 --- a/internal/logs/local_controller.go +++ b/internal/logs/local_controller.go @@ -24,7 +24,7 @@ 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 } diff --git a/internal/logs/microservice.go b/internal/logs/microservice.go index 2dcfc3edc..e0963336b 100644 --- a/internal/logs/microservice.go +++ b/internal/logs/microservice.go @@ -43,9 +43,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 } diff --git a/internal/prune/agent/execute.go b/internal/prune/agent/execute.go index 5c2faf11f..580a0a435 100644 --- a/internal/prune/agent/execute.go +++ b/internal/prune/agent/execute.go @@ -45,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 92e9f8879..4377a6134 100644 --- a/internal/prune/agent/local.go +++ b/internal/prune/agent/local.go @@ -3,22 +3,20 @@ 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 384e5f758..f9e3139b2 100644 --- a/internal/prune/agent/remote.go +++ b/internal/prune/agent/remote.go @@ -4,6 +4,7 @@ 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" @@ -15,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 @@ -29,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/util/client/api.go b/internal/util/client/api.go index 866275327..16173607f 100644 --- a/internal/util/client/api.go +++ b/internal/util/client/api.go @@ -165,7 +165,7 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura agentMapByUUID[agent.UUID] = *agent } - arch, found := rsc.ArchIntMap[agentInfo.ArchID] + arch, found := rsc.ArchIDToString(agentInfo.ArchID) if !found { arch = "auto" } @@ -281,6 +281,9 @@ 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, } return agentConfig, tags, agentStatus, err diff --git a/internal/util/client/client.go b/internal/util/client/client.go index 98020d645..18a8b7e1d 100644 --- a/internal/util/client/client.go +++ b/internal/util/client/client.go @@ -137,18 +137,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 @@ -168,6 +157,39 @@ func syncAgentInfo(namespace string) error { return config.Flush() } +// 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 diff --git a/internal/util/client/client_sync_test.go b/internal/util/client/client_sync_test.go new file mode 100644 index 000000000..0beb57ac2 --- /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.3", + }, + } + + 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.3" { + 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) + } +} From e0bfbbc898ad949fa6ad19a69ff610c2cfe1a149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Tue, 23 Jun 2026 00:22:13 +0300 Subject: [PATCH 25/63] Fix embedded iofog user registration to set a permanent password. EnsureIofogUserEmbedded: - Create new users with a random temporary password instead of the final password. - Finalize the account via admin reset-token and change-password with the intended password. - Skip only when the user already exists and mustChangePassword is false. - Re-run finalization for existing users still marked mustChangePassword. Controller auth client: - Return the created auth user from POST /users. - Add reset-token and change-password helpers for embedded provisioning. Tests: - Add temp password complexity coverage. - Extend embedded user tests for create, skip-ready, and finalize-stuck-user paths. --- internal/auth/controller_client.go | 37 +++++++++- internal/auth/embedded_user.go | 65 ++++++++++++++---- internal/auth/embedded_user_test.go | 103 ++++++++++++++++++++++------ internal/auth/temp_password.go | 83 ++++++++++++++++++++++ internal/auth/temp_password_test.go | 31 +++++++++ 5 files changed, 284 insertions(+), 35 deletions(-) create mode 100644 internal/auth/temp_password.go create mode 100644 internal/auth/temp_password_test.go diff --git a/internal/auth/controller_client.go b/internal/auth/controller_client.go index cdc9fe917..5266fbd28 100644 --- a/internal/auth/controller_client.go +++ b/internal/auth/controller_client.go @@ -75,9 +75,40 @@ func (c *controllerAuthClient) listAuthUsers() ([]client.AuthUserResponse, error return users, nil } -func (c *controllerAuthClient) createAuthUser(req client.AuthUserCreateRequest) error { - _, err := c.doJSON(http.MethodPost, "/users", authHeaders(c.accessToken), req) - return err +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) { diff --git a/internal/auth/embedded_user.go b/internal/auth/embedded_user.go index dab924fe7..5e70a5ab5 100644 --- a/internal/auth/embedded_user.go +++ b/internal/auth/embedded_user.go @@ -23,7 +23,9 @@ type EmbeddedAuthSpec struct { type embeddedAuthClient interface { bootstrapLogin(email, password string) error listAuthUsers() ([]client.AuthUserResponse, error) - createAuthUser(req client.AuthUserCreateRequest) 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) { @@ -66,22 +68,61 @@ func EnsureIofogUserEmbedded(ctx context.Context, namespace, endpoint string, sp if err != nil { return err } - if authUserExists(users, spec.User.Email) { + + existing, found := findAuthUserByEmail(users, spec.User.Email) + if found && !existing.MustChangePassword { return nil } - return clt.createAuthUser(client.AuthUserCreateRequest{ - Email: spec.User.Email, - Password: password, - Groups: []string{"admin"}, - }) + 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 authUserExists(users []client.AuthUserResponse, email string) bool { - for _, u := range users { - if u.Email == email { - return true +func findAuthUserByEmail(users []client.AuthUserResponse, email string) (*client.AuthUserResponse, bool) { + for i := range users { + if users[i].Email == email { + return &users[i], true } } - return false + return nil, false } diff --git a/internal/auth/embedded_user_test.go b/internal/auth/embedded_user_test.go index 1ba7ffb50..0c88a885c 100644 --- a/internal/auth/embedded_user_test.go +++ b/internal/auth/embedded_user_test.go @@ -12,20 +12,21 @@ import ( "golang.org/x/term" ) +const testTempPassword = "TempPass123!" + type mockEmbeddedAuthClient struct { - loginEmail string - loginPassword string - loginErr error - users []client.AuthUserResponse - created []client.AuthUserCreateRequest - loginCalls int - listCalls int + 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.loginCalls++ - m.loginEmail = email - m.loginPassword = password + m.logins = append(m.logins, [2]string{email, password}) if m.loginErr != nil { return m.loginErr } @@ -37,8 +38,26 @@ func (m *mockEmbeddedAuthClient) listAuthUsers() ([]client.AuthUserResponse, err return m.users, nil } -func (m *mockEmbeddedAuthClient) createAuthUser(req client.AuthUserCreateRequest) error { +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 } @@ -54,7 +73,7 @@ func TestEnsureIofogUserEmbeddedSkipsExternal(t *testing.T) { User: &rsc.IofogUser{Email: "user@domain.com"}, }) require.NoError(t, err) - require.Zero(t, mock.loginCalls) + require.Empty(t, mock.logins) } func TestEnsureIofogUserEmbeddedCreatesWhenMissing(t *testing.T) { @@ -63,6 +82,7 @@ func TestEnsureIofogUserEmbeddedCreatesWhenMissing(t *testing.T) { 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() @@ -73,19 +93,56 @@ func TestEnsureIofogUserEmbeddedCreatesWhenMissing(t *testing.T) { User: user, }) require.NoError(t, err) - require.Equal(t, 1, mock.loginCalls) - require.Equal(t, "admin", mock.loginEmail) - require.Equal(t, "BootstrapTest12!", mock.loginPassword) + 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, "LocalTest12!", mock.created[0].Password) + 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 TestEnsureIofogUserEmbeddedSkipsWhenExists(t *testing.T) { +func TestEnsureIofogUserEmbeddedFinalizesExistingTempUser(t *testing.T) { t.Cleanup(resetEmbeddedAuthDeps) mock := &mockEmbeddedAuthClient{ - users: []client.AuthUserResponse{{Email: "user@domain.com"}}, + users: []client.AuthUserResponse{{ + ID: "existing-user", + Email: "user@domain.com", + MustChangePassword: true, + }}, } newEmbeddedAuthClient = func(context.Context, string, string) (embeddedAuthClient, error) { return mock, nil @@ -100,8 +157,10 @@ func TestEnsureIofogUserEmbeddedSkipsWhenExists(t *testing.T) { User: user, }) require.NoError(t, err) - require.Equal(t, 1, mock.loginCalls) 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) { @@ -110,6 +169,7 @@ func TestEnsureIofogUserEmbeddedUsesInteractivePassword(t *testing.T) { 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) { @@ -131,7 +191,9 @@ func TestEnsureIofogUserEmbeddedUsesInteractivePassword(t *testing.T) { require.GreaterOrEqual(t, promptCalls, 2) require.NotEmpty(t, user.GetRawPassword()) require.Len(t, mock.created, 1) - require.Equal(t, "LocalTest12!", mock.created[0].Password) + 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) { @@ -156,6 +218,7 @@ 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/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) + } +} From aacf9477c997e7717473da69a85773cabe7bd700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Tue, 23 Jun 2026 10:36:59 +0300 Subject: [PATCH 26/63] Extend LocalControlPlane schema, validation, and YAML fixtures. --- internal/resource/controlplane_test.go | 46 ++- internal/resource/local_controlplane.go | 119 +++++-- .../local_controlplane_golden_test.go | 181 +++++++++++ .../resource/local_controlplane_validate.go | 304 ++++++++++++++++++ .../local/controlplane-datasance.yaml | 39 +++ .../testdata/local/controlplane-iofog.yaml | 26 ++ .../local/controlplane-private-registry.yaml | 43 +++ .../resource/testdata/local/controlplane.yaml | 48 +++ internal/resource/types.go | 24 +- internal/resource/yaml.go | 3 +- 10 files changed, 792 insertions(+), 41 deletions(-) create mode 100644 internal/resource/local_controlplane_golden_test.go create mode 100644 internal/resource/local_controlplane_validate.go create mode 100644 internal/resource/testdata/local/controlplane-datasance.yaml create mode 100644 internal/resource/testdata/local/controlplane-iofog.yaml create mode 100644 internal/resource/testdata/local/controlplane-private-registry.yaml create mode 100644 internal/resource/testdata/local/controlplane.yaml diff --git a/internal/resource/controlplane_test.go b/internal/resource/controlplane_test.go index fbe588c0e..8e12a9b4b 100644 --- a/internal/resource/controlplane_test.go +++ b/internal/resource/controlplane_test.go @@ -4,7 +4,6 @@ import ( "strings" "testing" - "github.com/eclipse-iofog/iofogctl/pkg/util" "gopkg.in/yaml.v2" ) @@ -210,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", @@ -221,7 +237,7 @@ func TestLocalControlPlane(t *testing.T) { } _ = 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" { @@ -244,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/local_controlplane.go b/internal/resource/local_controlplane.go index 1493e1dcd..cebd3f8de 100644 --- a/internal/resource/local_controlplane.go +++ b/internal/resource/local_controlplane.go @@ -1,18 +1,29 @@ package resource import ( + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) type LocalControlPlane struct { - CA string `yaml:"ca,omitempty"` - 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 { @@ -31,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 + } + for idx := range cp.Controllers { + if cp.Controllers[idx].Endpoint != "" { + return cp.Controllers[idx].Endpoint, nil + } } - return cp.Controller.GetEndpoint(), nil + return "", util.NewError("Local Control Plane does not have an endpoint") } func (cp *LocalControlPlane) UpdateController(baseController Controller) error { @@ -56,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 } @@ -65,38 +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..640405821 --- /dev/null +++ b/internal/resource/local_controlplane_validate.go @@ -0,0 +1,304 @@ +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" +) + +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 := validateLocalIofogUser(cp.IofogUser); err != nil { + return err + } + if err := validateLocalAuth(cp.Auth); err != nil { + return err + } + if err := validateLocalSystemAgent(cp.SystemAgent); err != nil { + return err + } + if err := validateLocalEndpoint(cp.Endpoint, cp.Controller.PublicUrl); err != nil { + return err + } + if err := validateControllerPackage(cp.Controller.Package); err != nil { + return err + } + if err := validateLocalDatabase(cp.Database); err != nil { + return err + } + if err := validateLocalSystemMicroservices(cp.SystemMicroservices); err != nil { + return err + } + if err := validateLocalCAField("ca", cp.CA); err != nil { + return err + } + if err := validateSiteCertificateBlock("routerSiteCA", cp.RouterSiteCA); err != nil { + return err + } + if err := validateSiteCertificateBlock("routerLocalCA", cp.RouterLocalCA); err != nil { + return err + } + if err := validateSiteCertificateBlock("natsSiteCA", cp.NatsSiteCA); err != nil { + return err + } + if err := validateSiteCertificateBlock("natsLocalCA", cp.NatsLocalCA); err != nil { + return err + } + if err := validateControlPlaneTLS(cp.TLS); err != nil { + return err + } + if err := validateLocalVault(cp.Vault); err != nil { + return err + } + return nil +} + +func validateLocalIofogUser(user IofogUser) error { + if user.Email == "" { + return util.NewInputError("Local Control Plane iofogUser.email is required") + } + if rawPassword := user.GetRawPassword(); rawPassword != "" { + if err := inputvalidate.ValidatePasswordComplexity(rawPassword); err != nil { + return err + } + } + return nil +} + +func validateLocalAuth(auth Auth) error { + switch auth.Mode { + case authModeEmbedded: + return validateEmbeddedAuth(auth) + case authModeExternal: + return validateExternalAuth(auth) + case "": + return util.NewInputError("Local Control Plane auth.mode is required (embedded or external)") + default: + return util.NewInputError(fmt.Sprintf("Local Control Plane auth.mode %q is invalid (embedded or external)", auth.Mode)) + } +} + +func validateEmbeddedAuth(auth Auth) error { + if auth.Bootstrap == nil { + return util.NewInputError("Local Control Plane auth.bootstrap is required when auth.mode is embedded") + } + if auth.Bootstrap.Username == "" { + return util.NewInputError("Local Control Plane auth.bootstrap.username is required when auth.mode is embedded") + } + if auth.Bootstrap.Password == "" { + return util.NewInputError("Local Control Plane auth.bootstrap.password is required in YAML when auth.mode is embedded") + } + return inputvalidate.ValidatePasswordComplexity(auth.Bootstrap.Password) +} + +func validateExternalAuth(auth Auth) error { + if auth.IssuerUrl == "" { + return util.NewInputError("Local Control Plane auth.issuerUrl is required when auth.mode is external") + } + if auth.Client == nil || auth.Client.ID == "" { + return util.NewInputError("Local Control Plane auth.client.id is required when auth.mode is external") + } + if auth.Client.Secret == "" { + return util.NewInputError("Local Control Plane 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 nil +} + +func validateLocalEndpoint(endpoint, publicURL string) error { + if endpoint != "" && publicURL != "" && endpoint != publicURL { + return util.NewInputError("Local Control Plane 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("Local Control Plane endpoint URL %q is invalid: %v", 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(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("Local Control Plane controller.package requires registry, username, and password for private registry access") + } + } + return nil +} + +func validateLocalDatabase(db Database) error { + if db.Provider == "" { + return nil + } + if _, ok := validDatabaseProviders[db.Provider]; !ok { + return util.NewInputError(fmt.Sprintf("Local Control Plane database.provider %q is invalid (postgres or mysql)", db.Provider)) + } + if db.User == "" || db.Host == "" || db.DatabaseName == "" || db.Password == "" || db.Port == 0 { + return util.NewInputError("Local Control Plane database requires user, host, port, password, and databaseName when provider is set") + } + return nil +} + +func validateLocalSystemMicroservices(sys install.RemoteSystemMicroservices) error { + return nil +} + +func validateLocalCAField(name, value string) error { + if value == "" { + return nil + } + if _, err := base64.StdEncoding.DecodeString(value); err != nil { + return util.NewInputError(fmt.Sprintf("Local Control Plane %s must be valid base64", name)) + } + return nil +} + +func validateSiteCertificateBlock(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("Local Control Plane %s.tlsCert must be valid base64", name)) + } + } + if cert.TLSKey != "" { + if _, err := base64.StdEncoding.DecodeString(cert.TLSKey); err != nil { + return util.NewInputError(fmt.Sprintf("Local Control Plane %s.tlsKey must be valid base64", name)) + } + } + return nil +} + +func validateControlPlaneTLS(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("Local Control Plane %s must be valid base64", value.name)) + } + } + if (tls.Cert == "") != (tls.Key == "") { + return util.NewInputError("Local Control Plane tls.cert and tls.key must both be set when either is provided") + } + return nil +} + +func validateLocalVault(vault *VaultSpec) error { + if vault == nil || vault.Enabled == nil || !*vault.Enabled { + return nil + } + if vault.Provider == "" { + return util.NewInputError("Local Control Plane vault.provider is required when vault.enabled is true") + } + if vault.BasePath == "" { + return util.NewInputError("Local Control Plane vault.basePath is required when vault.enabled is true") + } + if _, ok := validVaultProviders[strings.ToLower(vault.Provider)]; !ok { + return util.NewInputError(fmt.Sprintf("Local Control Plane vault.provider %q is invalid", vault.Provider)) + } + switch strings.ToLower(vault.Provider) { + case "hashicorp", "openbao", "vault": + if vault.Hashicorp == nil || vault.Hashicorp.Address == "" || vault.Hashicorp.Token == "" { + return util.NewInputError("Local Control Plane vault.hashicorp is required for the selected vault provider") + } + case "aws", "aws-secrets-manager": + if vault.Aws == nil || vault.Aws.Region == "" { + return util.NewInputError("Local Control Plane vault.aws is required for the selected vault provider") + } + case "azure", "azure-key-vault": + if vault.Azure == nil || vault.Azure.URL == "" { + return util.NewInputError("Local Control Plane vault.azure is required for the selected vault provider") + } + case "google", "google-secret-manager": + if vault.Google == nil || vault.Google.ProjectId == "" { + return util.NewInputError("Local Control Plane vault.google is required for the selected vault provider") + } + } + return nil +} diff --git a/internal/resource/testdata/local/controlplane-datasance.yaml b/internal/resource/testdata/local/controlplane-datasance.yaml new file mode 100644 index 000000000..476fb128f --- /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.1 + arm64: ghcr.io/datasance/nats:2.14.2-rc.1 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 + arm: ghcr.io/datasance/nats:2.14.2-rc.1 +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..d5b596f68 --- /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.1 + arm64: ghcr.io/datasance/nats:2.14.2-rc.1 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 + arm: ghcr.io/datasance/nats:2.14.2-rc.1 +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..120eb424f --- /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.2 + 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.1 + arm64: ghcr.io/datasance/nats:2.14.2-rc.1 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 + arm: ghcr.io/datasance/nats:2.14.2-rc.1 + 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/types.go b/internal/resource/types.go index 3c8ed0531..2e555dab0 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -363,10 +363,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). diff --git a/internal/resource/yaml.go b/internal/resource/yaml.go index 4247cb3f2..e0c81b499 100644 --- a/internal/resource/yaml.go +++ b/internal/resource/yaml.go @@ -44,7 +44,6 @@ func UnmarshallRemoteControlPlane(file []byte) (controlPlane RemoteControlPlane, } 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 +53,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 From ca8f0b24fa0d2014de9c2c433703429c47fb28e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Tue, 23 Jun 2026 10:37:03 +0300 Subject: [PATCH 27/63] Add edgelet manifest deploy helpers and harden local command execution. --- pkg/iofog/install/edgelet_deploy.go | 87 ++++++++++++++++++++++++ pkg/iofog/install/edgelet_deploy_test.go | 29 ++++++++ pkg/iofog/install/edgelet_local.go | 9 ++- pkg/iofog/install/edgelet_remote.go | 11 +++ pkg/iofog/install/edgelet_scripts.go | 12 +++- pkg/util/exec.go | 12 ++++ 6 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 pkg/iofog/install/edgelet_deploy.go create mode 100644 pkg/iofog/install/edgelet_deploy_test.go diff --git a/pkg/iofog/install/edgelet_deploy.go b/pkg/iofog/install/edgelet_deploy.go new file mode 100644 index 000000000..d17e0686c --- /dev/null +++ b/pkg/iofog/install/edgelet_deploy.go @@ -0,0 +1,87 @@ +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. +func WriteTempManifest(data []byte, prefix string) (path string, cleanup func(), err error) { + f, err := os.CreateTemp("", prefix+"-*.yaml") + if err != nil { + return "", nil, err + } + path = f.Name() + if _, err = f.Write(data); err != nil { + f.Close() + os.Remove(path) + return "", nil, err + } + if err = f.Close(); err != nil { + os.Remove(path) + return "", nil, err + } + return path, func() { _ = os.Remove(path) }, nil +} + +// 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..88ef10eaf --- /dev/null +++ b/pkg/iofog/install/edgelet_deploy_test.go @@ -0,0 +1,29 @@ +package install + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +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 index 68f4aa371..0b14299b2 100644 --- a/pkg/iofog/install/edgelet_local.go +++ b/pkg/iofog/install/edgelet_local.go @@ -216,16 +216,19 @@ func (agent *LocalEdgelet) Uninstall(removeData bool) error { agent.procs.setUninstallArgs(agent.cfg, removeData) cmd := agent.procs.Uninstall.getCommand() if needsLocalSudo(agent.cfg) { - cmd = "sudo " + cmd + 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, _ string) string { +func (agent *LocalEdgelet) edgeletCommand(cmd, msg string) string { prefix := "" if needsLocalSudo(agent.cfg) { - prefix = "sudo " + prefix = "sudo env PATH=$PATH:/usr/local/bin:/usr/bin:/sbin " } + Verbose(msg) return agent.cfg.bootstrapEnv(true) + " " + prefix + cmd } diff --git a/pkg/iofog/install/edgelet_remote.go b/pkg/iofog/install/edgelet_remote.go index 4f9d01a1c..58cbccad7 100644 --- a/pkg/iofog/install/edgelet_remote.go +++ b/pkg/iofog/install/edgelet_remote.go @@ -328,6 +328,17 @@ func (agent *RemoteEdgelet) Deprovision() error { 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", diff --git a/pkg/iofog/install/edgelet_scripts.go b/pkg/iofog/install/edgelet_scripts.go index 804a81e2f..a79d401ea 100644 --- a/pkg/iofog/install/edgelet_scripts.go +++ b/pkg/iofog/install/edgelet_scripts.go @@ -380,5 +380,15 @@ func isEdgeletNotProvisionedError(err error) bool { if err == nil { return false } - return strings.Contains(err.Error(), "not provisioned") + 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/util/exec.go b/pkg/util/exec.go index 66aa72590..87a9a3bfc 100644 --- a/pkg/util/exec.go +++ b/pkg/util/exec.go @@ -2,12 +2,18 @@ package util import ( "bytes" + "fmt" "os" "os/exec" + "strings" ) // Exec command func Exec(env, cmdName string, args ...string) (stdout bytes.Buffer, err error) { + if IsDebug() { + fmt.Printf("[LOCAL]: Running: %s %s\n", cmdName, strings.Join(args, " ")) + } + // Instantiate command object cmd := exec.Command(cmdName, args...) @@ -23,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 } From 2654bc4424d2754d770ba70fc8ca9a62c2996191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Tue, 23 Jun 2026 10:37:07 +0300 Subject: [PATCH 28/63] Deploy local control plane via edgelet translate, registry, and system agent. --- internal/deploy/controller/local/local.go | 19 +- .../deploy/controller/local/local_test.go | 64 +++ .../deploy/controlplane/local/edgelet_host.go | 230 +++++++++++ internal/deploy/controlplane/local/execute.go | 188 +++------ .../deploy/controlplane/local/manifest.go | 185 +++++++++ .../deploy/controlplane/local/system_agent.go | 193 +++++++++ .../controlplane/local/system_agent_test.go | 56 +++ .../local/testdata/edgelet-cp-datasance.yaml | 37 ++ .../local/testdata/edgelet-cp-iofog.yaml | 35 ++ .../testdata/edgelet-registry-private.yaml | 8 + .../deploy/controlplane/local/translate.go | 389 ++++++++++++++++++ .../controlplane/local/translate_test.go | 212 ++++++++++ 12 files changed, 1472 insertions(+), 144 deletions(-) create mode 100644 internal/deploy/controller/local/local_test.go create mode 100644 internal/deploy/controlplane/local/edgelet_host.go create mode 100644 internal/deploy/controlplane/local/manifest.go create mode 100644 internal/deploy/controlplane/local/system_agent.go create mode 100644 internal/deploy/controlplane/local/system_agent_test.go create mode 100644 internal/deploy/controlplane/local/testdata/edgelet-cp-datasance.yaml create mode 100644 internal/deploy/controlplane/local/testdata/edgelet-cp-iofog.yaml create mode 100644 internal/deploy/controlplane/local/testdata/edgelet-registry-private.yaml create mode 100644 internal/deploy/controlplane/local/translate.go create mode 100644 internal/deploy/controlplane/local/translate_test.go diff --git a/internal/deploy/controller/local/local.go b/internal/deploy/controller/local/local.go index 1f4225fcf..1a80428a6 100644 --- a/internal/deploy/controller/local/local.go +++ b/internal/deploy/controller/local/local.go @@ -179,12 +179,10 @@ func (exe *localExecutor) Execute() error { 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 +func localSystemImagesToInstall(s install.RemoteSystemMicroservices, nats *rsc.NatsEnabledConfig) *install.LocalSystemImages { + out := &install.LocalSystemImages{ + Router: firstSystemImage(s.Router), + Nats: firstSystemImage(s.Nats), } if nats != nil && nats.Enabled != nil { out.NatsEnabled = nats.Enabled @@ -192,6 +190,15 @@ func localSystemImagesToInstall(s *rsc.LocalSystemMicroservices, nats *rsc.NatsE return out } +func firstSystemImage(images install.RemoteSystemImages) string { + for _, image := range []string{images.AMD64, images.ARM64, images.RISCV64, images.ARM} { + if image != "" { + return image + } + } + return "" +} + func Validate(ctrl rsc.Controller) error { if err := util.IsLowerAlphanumeric("Controller", ctrl.GetName()); err != nil { return err 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/controlplane/local/edgelet_host.go b/internal/deploy/controlplane/local/edgelet_host.go new file mode 100644 index 000000000..c4ad89150 --- /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 := install.WriteTempManifest(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 := install.WriteTempManifest(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 ff0a5a022..67a0752e0 100644 --- a/internal/deploy/controlplane/local/execute.go +++ b/internal/deploy/controlplane/local/execute.go @@ -1,140 +1,79 @@ 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" - "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: %w", err) +func (exe localControlPlaneExecutor) Execute() (err error) { + util.SpinStart(fmt.Sprintf("Deploying controlplane %s", exe.GetName())) + + if ca := exe.controlPlane.GetTrustCA(); ca != "" { + if err := trust.StoreCA(exe.namespace, ca); err != nil { + return err } } - if URL.Scheme == "" { - URL.Scheme = "http" + edgelet, err := installHostEdgelet(exe.controlPlane, exe.name) + if err != nil { + return err } - host := "" - if strings.Contains(URL.Host, ":") { - host, _, err = net.SplitHostPort(URL.Host) - if err != nil { - return "", fmt.Errorf("failed to split host and port: %w", err) - } - } else { - host = URL.Host + translateOpts := TranslateOptions{ + Name: exe.name, + Namespace: exe.namespace, } - // Add port for localhost - if util.IsLocalHost(host) { - host = net.JoinHostPort(host, iofog.ControllerHostECNViewerPortString) + registryID, err := deployPrivateEdgeletRegistry(exe.controlPlane, edgelet, translateOpts) + if err != nil { + return err } - URL.Host = host - return URL.String(), nil -} - -// updateViewerClientRootURL is retired in v3.8 (Keycloak viewer client). -func updateViewerClientRootURL(_ *rsc.LocalControlPlane, _ string) error { - 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 := deployEdgeletControlPlane(exe.controlPlane, edgelet, translateOpts, registryID); 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 + } -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 @@ -144,63 +83,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..5f36efeac --- /dev/null +++ b/internal/deploy/controlplane/local/system_agent.go @@ -0,0 +1,193 @@ +package deploylocalcontrolplane + +import ( + "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" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +const ( + deploymentTypeNative = "native" + deploymentTypeContainer = "container" + + defaultNatsServerPort = 4222 + defaultNatsClusterPort = 6222 + defaultNatsLeafPort = 7422 + defaultNatsMqttPort = 8883 + defaultNatsHTTPPort = 8222 + defaultJsStorageSize = "10G" + defaultJsMemoryStoreSize = "1G" +) + +func applyLocalSystemAgentNatsDefaults(cfg *rsc.AgentConfiguration) { + 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) + } +} + +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.RouterMode == nil { + interior := iofog.RouterModeInterior + deployAgentConfig.RouterMode = &interior + } else if *deployAgentConfig.RouterMode != iofog.RouterModeInterior { + interior := iofog.RouterModeInterior + deployAgentConfig.RouterMode = &interior + } + + if deployAgentConfig.UpstreamRouters == nil { + emptyRouters := []string{} + deployAgentConfig.UpstreamRouters = &emptyRouters + } + if deployAgentConfig.UpstreamNatsServers == nil { + emptyNats := []string{} + deployAgentConfig.UpstreamNatsServers = &emptyNats + } + + if deployAgentConfig.EdgeRouterPort == nil { + edgeRouterPort := 45671 + deployAgentConfig.EdgeRouterPort = &edgeRouterPort + } + if deployAgentConfig.InterRouterPort == nil { + interRouterPort := 55671 + deployAgentConfig.InterRouterPort = &interRouterPort + } + if deployAgentConfig.MessagingPort == nil { + messagingPort := 5671 + deployAgentConfig.MessagingPort = &messagingPort + } + + applyLocalSystemAgentNatsDefaults(&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 + } + + if _, err := edgelet.Configure(endpoint, user); err != nil { + return fmt.Errorf("failed to provision system agent: %w", err) + } + return persistLocalSystemAgent(namespace, cp, name, deployAgentConfig, endpoint) +} + +func persistLocalSystemAgent(namespace string, cp *rsc.LocalControlPlane, name string, deployAgentConfig rsc.AgentConfiguration, endpoint string) error { + ns, err := config.GetNamespace(namespace) + if err != nil { + return err + } + + 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) + } + + configCopy := deployAgentConfig + host := agentInfo.Host + if deployAgentConfig.Host != nil && strings.TrimSpace(*deployAgentConfig.Host) != "" { + host = *deployAgentConfig.Host + } + + agent := &rsc.LocalAgent{ + Name: name, + UUID: agentInfo.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..14f2349de --- /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.1 + arm64: ghcr.io/datasance/nats:2.14.2-rc.1 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 + arm: ghcr.io/datasance/nats:2.14.2-rc.1 + 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..137d9573a --- /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.1 + arm64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + riscv64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + arm: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + 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..01a87dc81 --- /dev/null +++ b/internal/deploy/controlplane/local/translate.go @@ -0,0 +1,389 @@ +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). +func TranslateEdgeletControlPlaneManifest(cp *resource.LocalControlPlane, opts TranslateOptions) edgeletControlPlaneManifest { + return translateEdgeletControlPlane(cp, opts.mergeDefaults(opts.Namespace)) +} + +// TranslateEdgeletRegistryManifest returns the edgelet Registry manifest struct (test helper). +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..a20efbb98 --- /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.2" + testRouterImage = "ghcr.io/datasance/router:3.8.0-rc.1" + testNatsImage = "ghcr.io/datasance/nats:2.14.2-rc.1" + + 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.1" +) + +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) +} From 72c5bf46c9bca51b25fab0f6c691b53dc90afb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Tue, 23 Jun 2026 10:37:12 +0300 Subject: [PATCH 29/63] Route LocalControlPlane deploy through validation and dedicated system agent setup. --- internal/deploy/agent/edgelet.go | 14 +- internal/deploy/execute.go | 139 ++++++++---------- internal/deploy/execute_agent_config_test.go | 35 +++++ internal/deploy/validate/local_agent.go | 18 +++ internal/deploy/validate/local_agent_test.go | 39 +++++ .../deploy/validate/local_controlplane.go | 15 ++ .../validate/local_controlplane_test.go | 34 +++++ 7 files changed, 201 insertions(+), 93 deletions(-) create mode 100644 internal/deploy/execute_agent_config_test.go create mode 100644 internal/deploy/validate/local_agent.go create mode 100644 internal/deploy/validate/local_agent_test.go create mode 100644 internal/deploy/validate/local_controlplane.go create mode 100644 internal/deploy/validate/local_controlplane_test.go diff --git a/internal/deploy/agent/edgelet.go b/internal/deploy/agent/edgelet.go index f2d6bc614..a90863424 100644 --- a/internal/deploy/agent/edgelet.go +++ b/internal/deploy/agent/edgelet.go @@ -3,13 +3,12 @@ package deployagent import ( "strings" + 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" ) -const localAgentConflictPort = 54321 - type edgeletAgent interface { Bootstrap() error Configure(controllerEndpoint string, user install.IofogUser) (string, error) @@ -43,16 +42,7 @@ func ensureLocalAgentHost(agent *rsc.LocalAgent) error { } func checkLocalAgentPortAvailable(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 + return deployvalidate.LocalAgentPortAvailable(isSystem) } func applyEdgeletPackage(agent edgeletAgent, pkg rsc.Package) error { diff --git a/internal/deploy/execute.go b/internal/deploy/execute.go index 7c2676daa..5bd9295b2 100644 --- a/internal/deploy/execute.go +++ b/internal/deploy/execute.go @@ -2,6 +2,7 @@ package deploy import ( "fmt" + "strings" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/config" @@ -28,6 +29,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" @@ -98,7 +100,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) { @@ -187,41 +189,26 @@ func Execute(opt *Options) (err error) { 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.Host != nil && *deployConfig.Host != "" { - apiHost = *deployConfig.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 { @@ -234,66 +221,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 } } @@ -555,3 +515,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/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") +} From d3a1eb86dbfb57a2dca8604431e0330e8ad2ff14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Tue, 23 Jun 2026 10:37:17 +0300 Subject: [PATCH 30/63] Sync local control plane agents from controller backend on connect. --- internal/util/client/api.go | 7 ++ internal/util/client/client.go | 88 +++++++++++++++++-- .../util/client/client_local_cp_sync_test.go | 70 +++++++++++++++ internal/util/client/pkg.go | 9 ++ 4 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 internal/util/client/client_local_cp_sync_test.go diff --git a/internal/util/client/api.go b/internal/util/client/api.go index 16173607f..b04f61000 100644 --- a/internal/util/client/api.go +++ b/internal/util/client/api.go @@ -16,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) diff --git a/internal/util/client/client.go b/internal/util/client/client.go index 18a8b7e1d..f8b314cdc 100644 --- a/internal/util/client/client.go +++ b/internal/util/client/client.go @@ -49,6 +49,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{} @@ -80,18 +86,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 } } @@ -107,9 +107,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) @@ -157,6 +156,77 @@ 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 { 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..16bb316b7 --- /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.3", + }, + } + + 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/pkg.go b/internal/util/client/pkg.go index db3bdeb20..7f1cda994 100644 --- a/internal/util/client/pkg.go +++ b/internal/util/client/pkg.go @@ -50,6 +50,7 @@ func (ccr *clientCacheResult) get() (*client.Client, error) { type agentCacheRequest struct { namespace string + invalidate bool resultChan chan *agentCacheResult } @@ -60,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 From f4fc2bc232adac133fef0efe99febadf5292254e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Tue, 23 Jun 2026 10:37:21 +0300 Subject: [PATCH 31/63] Split agent delete into deprovision and uninstall and add edgelet CP teardown. --- internal/delete/agent/execute.go | 66 ++++++-- internal/delete/agent/local.go | 25 ++- internal/delete/agent/remote.go | 39 ++++- internal/delete/agents/agents.go | 61 +++++++ internal/delete/all/all.go | 21 ++- internal/delete/controller/local.go | 23 +-- internal/delete/controlplane/local/local.go | 22 ++- .../delete/controlplane/local/teardown.go | 149 ++++++++++++++++++ 8 files changed, 368 insertions(+), 38 deletions(-) create mode 100644 internal/delete/agents/agents.go create mode 100644 internal/delete/controlplane/local/teardown.go diff --git a/internal/delete/agent/execute.go b/internal/delete/agent/execute.go index e49a4d9c3..b7b37a7f5 100644 --- a/internal/delete/agent/execute.go +++ b/internal/delete/agent/execute.go @@ -2,11 +2,14 @@ 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" ) @@ -56,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 } @@ -66,27 +76,45 @@ func (exe executor) Execute() (err error) { } } - // Remove from Controller switch agent := baseAgent.(type) { case *rsc.LocalAgent: - if err = exe.deleteLocalEdgelet(agent); err != nil { - util.PrintInfo(fmt.Sprintf("Could not remove Agent from the local host %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 } @@ -122,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 338bc5131..f2a38165e 100644 --- a/internal/delete/agent/local.go +++ b/internal/delete/agent/local.go @@ -10,7 +10,23 @@ import ( "github.com/eclipse-iofog/iofogctl/pkg/util" ) -func (exe executor) deleteLocalEdgelet(agent *rsc.LocalAgent) error { +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 +} + +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 { @@ -40,3 +56,10 @@ func (exe executor) deleteLocalEdgelet(agent *rsc.LocalAgent) 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 9f20f2406..f6a092480 100644 --- a/internal/delete/agent/remote.go +++ b/internal/delete/agent/remote.go @@ -9,14 +9,9 @@ import ( "github.com/eclipse-iofog/iofogctl/pkg/util" ) -func (exe executor) deleteRemoteAgent(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.") - return nil - } - +func (exe executor) newRemoteEdgelet(agent *rsc.RemoteAgent) (*install.RemoteEdgelet, error) { cfg := deployairgap.EdgeletInstallConfig("linux", agent.Config, agent.Package) - edgelet, err := install.NewRemoteEdgelet( + return install.NewRemoteEdgelet( agent.SSH.User, agent.Host, agent.SSH.Port, @@ -25,6 +20,29 @@ func (exe executor) deleteRemoteAgent(agent *rsc.RemoteAgent) error { agent.UUID, cfg, ) +} + +func (exe executor) deprovisionRemoteEdgelet(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.") + 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 } @@ -33,3 +51,10 @@ func (exe executor) deleteRemoteAgent(agent *rsc.RemoteAgent) 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 7774c7257..c4f61129b 100644 --- a/internal/delete/all/all.go +++ b/internal/delete/all/all.go @@ -3,9 +3,12 @@ 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" ) @@ -56,13 +59,23 @@ func Execute(namespace string, useDetached, force, deleteNamespace bool) error { } } - // 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 } diff --git a/internal/delete/controller/local.go b/internal/delete/controller/local.go index f2224e805..361247dd2 100644 --- a/internal/delete/controller/local.go +++ b/internal/delete/controller/local.go @@ -31,24 +31,25 @@ func (exe *LocalExecutor) GetName() string { } func (exe *LocalExecutor) Execute() error { - ns, err := config.GetNamespace(exe.namespace) - if err != nil { - return err + if err := exe.deleteLegacyControllerContainer(); err != nil { + util.PrintNotify(fmt.Sprintf("Could not clean Controller container: %v", err)) } - client, err := install.NewLocalContainerClient(install.DefaultLocalContainerEngine, nil) + + ns, err := config.GetNamespace(exe.namespace) 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 } ns.SetControlPlane(exe.controlPlane) return config.Flush() } + +func (exe *LocalExecutor) deleteLegacyControllerContainer() error { + client, err := install.NewLocalContainerClient(install.DefaultLocalContainerEngine, nil) + if err != nil { + return err + } + return client.CleanContainer(exe.localControllerConfig.ContainerName) +} diff --git a/internal/delete/controlplane/local/local.go b/internal/delete/controlplane/local/local.go index 9333df104..9f2718f67 100644 --- a/internal/delete/controlplane/local/local.go +++ b/internal/delete/controlplane/local/local.go @@ -24,9 +24,7 @@ func (exe *Executor) GetName() string { return "Delete Control Plane" } -// Execute deletes application by deleting its associated application func (exe *Executor) Execute() (err error) { - // Get Control Plane ns, err := config.GetNamespace(exe.namespace) if err != nil { return err @@ -41,12 +39,24 @@ 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 + } + } else { + executor := deletecontroller.NewLocalExecutor(controlPlane, exe.namespace, name) + if err := executor.Execute(); 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" +} From 06475a8e2efafb5742507522670d0bfa69db1195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Tue, 23 Jun 2026 10:37:26 +0300 Subject: [PATCH 32/63] Enable install package verbosity when debug flag is set. --- internal/cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 72b5686ce..d425849c7 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -118,7 +118,7 @@ func initialize() { }, }) client.SetVerbosity(debug) - install.SetVerbosity(verbose) + install.SetVerbosity(verbose || debug) util.SpinEnable(!verbose && !debug) util.SetDebug(debug) } From dc0cdef4e01070a325fa37008adafd6547744c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Wed, 24 Jun 2026 01:05:55 +0300 Subject: [PATCH 33/63] Align RemoteControlPlane spec with v3.8 and add shared validation. Move global controller, TLS, and CA fields onto the control plane resource and add validation for multi-host deploy and controller add-ons. --- internal/resource/agent_validate.go | 21 ++ .../resource/local_controlplane_validate.go | 109 +++++----- internal/resource/remote_controller.go | 47 ++--- internal/resource/remote_controlplane.go | 77 ++++++- .../remote_controlplane_addon_test.go | 75 +++++++ .../remote_controlplane_golden_test.go | 190 ++++++++++++++++++ .../resource/remote_controlplane_validate.go | 153 ++++++++++++++ .../remote/controlplane-datasance.yaml | 58 ++++++ .../testdata/remote/controlplane-iofog.yaml | 58 ++++++ .../testdata/remote/controlplane-single.yaml | 23 +++ .../testdata/remote/controlplane.yaml | 82 ++++++++ internal/resource/yaml.go | 7 +- 12 files changed, 799 insertions(+), 101 deletions(-) create mode 100644 internal/resource/agent_validate.go create mode 100644 internal/resource/remote_controlplane_addon_test.go create mode 100644 internal/resource/remote_controlplane_golden_test.go create mode 100644 internal/resource/remote_controlplane_validate.go create mode 100644 internal/resource/testdata/remote/controlplane-datasance.yaml create mode 100644 internal/resource/testdata/remote/controlplane-iofog.yaml create mode 100644 internal/resource/testdata/remote/controlplane-single.yaml create mode 100644 internal/resource/testdata/remote/controlplane.yaml 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/local_controlplane_validate.go b/internal/resource/local_controlplane_validate.go index 640405821..910d9cebe 100644 --- a/internal/resource/local_controlplane_validate.go +++ b/internal/resource/local_controlplane_validate.go @@ -13,8 +13,9 @@ import ( ) const ( - authModeEmbedded = "embedded" - authModeExternal = "external" + authModeEmbedded = "embedded" + authModeExternal = "external" + localControlPlaneLabel = "Local Control Plane" ) var validDatabaseProviders = map[string]struct{}{ @@ -50,54 +51,54 @@ func ValidateLocalControlPlaneMetadata(fullYAML []byte) error { // ValidateLocalControlPlane validates a parsed LocalControlPlane spec. func ValidateLocalControlPlane(cp *LocalControlPlane) error { - if err := validateLocalIofogUser(cp.IofogUser); err != nil { + if err := validateIofogUser(localControlPlaneLabel, cp.IofogUser); err != nil { return err } - if err := validateLocalAuth(cp.Auth); err != nil { + if err := validateAuth(localControlPlaneLabel, cp.Auth); err != nil { return err } if err := validateLocalSystemAgent(cp.SystemAgent); err != nil { return err } - if err := validateLocalEndpoint(cp.Endpoint, cp.Controller.PublicUrl); err != nil { + if err := validateEndpointMatch(localControlPlaneLabel, cp.Endpoint, cp.Controller.PublicUrl); err != nil { return err } - if err := validateControllerPackage(cp.Controller.Package); err != nil { + if err := validateControllerPackage(localControlPlaneLabel, cp.Controller.Package); err != nil { return err } - if err := validateLocalDatabase(cp.Database); err != nil { + if err := validateDatabase(localControlPlaneLabel, cp.Database); err != nil { return err } if err := validateLocalSystemMicroservices(cp.SystemMicroservices); err != nil { return err } - if err := validateLocalCAField("ca", cp.CA); err != nil { + if err := validateCAField(localControlPlaneLabel, "ca", cp.CA); err != nil { return err } - if err := validateSiteCertificateBlock("routerSiteCA", cp.RouterSiteCA); err != nil { + if err := validateSiteCertificateBlock(localControlPlaneLabel, "routerSiteCA", cp.RouterSiteCA); err != nil { return err } - if err := validateSiteCertificateBlock("routerLocalCA", cp.RouterLocalCA); err != nil { + if err := validateSiteCertificateBlock(localControlPlaneLabel, "routerLocalCA", cp.RouterLocalCA); err != nil { return err } - if err := validateSiteCertificateBlock("natsSiteCA", cp.NatsSiteCA); err != nil { + if err := validateSiteCertificateBlock(localControlPlaneLabel, "natsSiteCA", cp.NatsSiteCA); err != nil { return err } - if err := validateSiteCertificateBlock("natsLocalCA", cp.NatsLocalCA); err != nil { + if err := validateSiteCertificateBlock(localControlPlaneLabel, "natsLocalCA", cp.NatsLocalCA); err != nil { return err } - if err := validateControlPlaneTLS(cp.TLS); err != nil { + if err := validateControlPlaneTLS(localControlPlaneLabel, cp.TLS); err != nil { return err } - if err := validateLocalVault(cp.Vault); err != nil { + if err := validateVault(localControlPlaneLabel, cp.Vault); err != nil { return err } return nil } -func validateLocalIofogUser(user IofogUser) error { +func validateIofogUser(label string, user IofogUser) error { if user.Email == "" { - return util.NewInputError("Local Control Plane iofogUser.email is required") + return util.NewInputError(label + " iofogUser.email is required") } if rawPassword := user.GetRawPassword(); rawPassword != "" { if err := inputvalidate.ValidatePasswordComplexity(rawPassword); err != nil { @@ -107,41 +108,41 @@ func validateLocalIofogUser(user IofogUser) error { return nil } -func validateLocalAuth(auth Auth) error { +func validateAuth(label string, auth Auth) error { switch auth.Mode { case authModeEmbedded: - return validateEmbeddedAuth(auth) + return validateEmbeddedAuth(label, auth) case authModeExternal: - return validateExternalAuth(auth) + return validateExternalAuth(label, auth) case "": - return util.NewInputError("Local Control Plane auth.mode is required (embedded or external)") + return util.NewInputError(label + " auth.mode is required (embedded or external)") default: - return util.NewInputError(fmt.Sprintf("Local Control Plane auth.mode %q is invalid (embedded or external)", auth.Mode)) + return util.NewInputError(fmt.Sprintf("%s auth.mode %q is invalid (embedded or external)", label, auth.Mode)) } } -func validateEmbeddedAuth(auth Auth) error { +func validateEmbeddedAuth(label string, auth Auth) error { if auth.Bootstrap == nil { - return util.NewInputError("Local Control Plane auth.bootstrap is required when auth.mode is embedded") + return util.NewInputError(label + " auth.bootstrap is required when auth.mode is embedded") } if auth.Bootstrap.Username == "" { - return util.NewInputError("Local Control Plane auth.bootstrap.username is required when auth.mode is embedded") + return util.NewInputError(label + " auth.bootstrap.username is required when auth.mode is embedded") } if auth.Bootstrap.Password == "" { - return util.NewInputError("Local Control Plane auth.bootstrap.password is required in YAML when auth.mode is embedded") + return util.NewInputError(label + " auth.bootstrap.password is required in YAML when auth.mode is embedded") } return inputvalidate.ValidatePasswordComplexity(auth.Bootstrap.Password) } -func validateExternalAuth(auth Auth) error { +func validateExternalAuth(label string, auth Auth) error { if auth.IssuerUrl == "" { - return util.NewInputError("Local Control Plane auth.issuerUrl is required when auth.mode is external") + return util.NewInputError(label + " auth.issuerUrl is required when auth.mode is external") } if auth.Client == nil || auth.Client.ID == "" { - return util.NewInputError("Local Control Plane auth.client.id is required when auth.mode is external") + return util.NewInputError(label + " auth.client.id is required when auth.mode is external") } if auth.Client.Secret == "" { - return util.NewInputError("Local Control Plane auth.client.secret is required when auth.mode is external") + return util.NewInputError(label + " auth.client.secret is required when auth.mode is external") } return nil } @@ -156,19 +157,19 @@ func validateLocalSystemAgent(systemAgent *SystemAgentConfig) error { 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 nil + return validateSystemAgentRouterNats(localControlPlaneLabel, systemAgent.AgentConfiguration) } -func validateLocalEndpoint(endpoint, publicURL string) error { +func validateEndpointMatch(label, endpoint, publicURL string) error { if endpoint != "" && publicURL != "" && endpoint != publicURL { - return util.NewInputError("Local Control Plane spec.endpoint must match spec.controller.publicUrl when both are set") + 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("Local Control Plane endpoint URL %q is invalid: %v", value, err)) + return util.NewInputError(fmt.Sprintf("%s endpoint URL %q is invalid: %v", label, value, err)) } } return nil @@ -185,7 +186,7 @@ func validateOptionalURL(raw string) error { return nil } -func validateControllerPackage(pkg *ControllerPackage) error { +func validateControllerPackage(label string, pkg *ControllerPackage) error { if pkg == nil { return nil } @@ -194,21 +195,21 @@ func validateControllerPackage(pkg *ControllerPackage) error { hasPassword := pkg.Password != "" if hasRegistry || hasUsername || hasPassword { if !hasRegistry || !hasUsername || !hasPassword { - return util.NewInputError("Local Control Plane controller.package requires registry, username, and password for private registry access") + return util.NewInputError(label + " controller.package requires registry, username, and password for private registry access") } } return nil } -func validateLocalDatabase(db Database) error { +func validateDatabase(label string, db Database) error { if db.Provider == "" { return nil } if _, ok := validDatabaseProviders[db.Provider]; !ok { - return util.NewInputError(fmt.Sprintf("Local Control Plane database.provider %q is invalid (postgres or mysql)", db.Provider)) + 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("Local Control Plane database requires user, host, port, password, and databaseName when provider is set") + return util.NewInputError(label + " database requires user, host, port, password, and databaseName when provider is set") } return nil } @@ -217,34 +218,34 @@ func validateLocalSystemMicroservices(sys install.RemoteSystemMicroservices) err return nil } -func validateLocalCAField(name, value string) error { +func validateCAField(label, name, value string) error { if value == "" { return nil } if _, err := base64.StdEncoding.DecodeString(value); err != nil { - return util.NewInputError(fmt.Sprintf("Local Control Plane %s must be valid base64", name)) + return util.NewInputError(fmt.Sprintf("%s %s must be valid base64", label, name)) } return nil } -func validateSiteCertificateBlock(name string, cert *SiteCertificate) error { +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("Local Control Plane %s.tlsCert must be valid base64", name)) + 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("Local Control Plane %s.tlsKey must be valid base64", name)) + return util.NewInputError(fmt.Sprintf("%s %s.tlsKey must be valid base64", label, name)) } } return nil } -func validateControlPlaneTLS(tls *ControlPlaneTLS) error { +func validateControlPlaneTLS(label string, tls *ControlPlaneTLS) error { if tls == nil { return nil } @@ -260,44 +261,44 @@ func validateControlPlaneTLS(tls *ControlPlaneTLS) error { continue } if _, err := base64.StdEncoding.DecodeString(value.raw); err != nil { - return util.NewInputError(fmt.Sprintf("Local Control Plane %s must be valid base64", value.name)) + return util.NewInputError(fmt.Sprintf("%s %s must be valid base64", label, value.name)) } } if (tls.Cert == "") != (tls.Key == "") { - return util.NewInputError("Local Control Plane tls.cert and tls.key must both be set when either is provided") + return util.NewInputError(label + " tls.cert and tls.key must both be set when either is provided") } return nil } -func validateLocalVault(vault *VaultSpec) error { +func validateVault(label string, vault *VaultSpec) error { if vault == nil || vault.Enabled == nil || !*vault.Enabled { return nil } if vault.Provider == "" { - return util.NewInputError("Local Control Plane vault.provider is required when vault.enabled is true") + return util.NewInputError(label + " vault.provider is required when vault.enabled is true") } if vault.BasePath == "" { - return util.NewInputError("Local Control Plane vault.basePath is required when vault.enabled is true") + 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("Local Control Plane vault.provider %q is invalid", vault.Provider)) + 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("Local Control Plane vault.hashicorp is required for the selected vault provider") + 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("Local Control Plane vault.aws is required for the selected vault provider") + 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("Local Control Plane vault.azure is required for the selected vault provider") + 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("Local Control Plane vault.google is required for the selected vault provider") + return util.NewInputError(label + " vault.google is required for the selected vault provider") } } return nil diff --git a/internal/resource/remote_controller.go b/internal/resource/remote_controller.go index 27839a994..2acffef8f 100644 --- a/internal/resource/remote_controller.go +++ b/internal/resource/remote_controller.go @@ -1,15 +1,9 @@ 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 @@ -17,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 { @@ -45,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 } @@ -57,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 03814be02..22897c7e0 100644 --- a/internal/resource/remote_controlplane.go +++ b/internal/resource/remote_controlplane.go @@ -1,6 +1,8 @@ package resource import ( + "fmt" + "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -8,17 +10,22 @@ 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"` } @@ -56,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") } @@ -120,21 +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/remote/controlplane-datasance.yaml b/internal/resource/testdata/remote/controlplane-datasance.yaml new file mode 100644 index 000000000..602da599b --- /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.1 + arm64: ghcr.io/datasance/nats:2.14.2-rc.1 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 + arm: ghcr.io/datasance/nats:2.14.2-rc.1 +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..99861f0db --- /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.1 + arm64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + riscv64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + arm: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 +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..68032548c --- /dev/null +++ b/internal/resource/testdata/remote/controlplane.yaml @@ -0,0 +1,82 @@ +apiVersion: datasance.com/v3 +kind: ControlPlane +metadata: + name: iofog +spec: + # endpoint: https://controller.example.com + iofogUser: + name: Foo + surname: Bar + email: user@domain.com + password: "TestPassword12!" + controller: + publicUrl: https://192.168.139.85:51121 + consoleUrl: https://192.168.139.85:80 + logLevel: info + package: + image: docker.io/emirhandurmus/controller:3.8.0-rc.3 + 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.1 + arm64: ghcr.io/datasance/nats:2.14.2-rc.1 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 + arm: ghcr.io/datasance/nats:2.14.2-rc.1 + routerSiteCA: + tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURPVENDQWlHZ0F3SUJBZ0lSQUtqeDNlUitiWEpmQ2pTSkZ6T3kxc2d3RFFZSktvWklodmNOQVFFTEJRQXcKR1RFWE1CVUdBMVVFQXhNT2NtOTFkR1Z5TFhOcGRHVXRZMkV3SGhjTk1qWXdNekV4TVRBME1ERTRXaGNOTXpFdwpNekV3TVRBME1ERTRXakFaTVJjd0ZRWURWUVFERXc1eWIzVjBaWEl0YzJsMFpTMWpZVENDQVNJd0RRWUpLb1pJCmh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBS2pwZlRHUVFJRFZRYTlRenAwQkxSZGI3TEZRS010eExiemUKY0hXeXAyUUp1VXZudm1LRG1rN3FuRkVYaVdpdFRLOVJPRCt6TWExMXF3M0lkb3ljNlhVZk42Y25UT1Z5YkE1bQp1QnZMQmV5eWhTcmVFYTV0dzVZVFNzbVhqRXJZek5LVzM0UWZIcmFvSTByYmhZL2Y5UXNwZUhXSkdiWlRiRlUwCis2N0lma0dCUlZGZUNRS3BEcEVMNVJ0cUpZOHFzanhXSlJ2NGYwaVE4UGYwUnhCSS9iZ0pBem42YS8vSVNFeUYKRkUxN2RBMXMzeGJsdFBPWU5CYll1bUtFd1lpTnRvSE9veWt1c2dodTFMOWVjU01WeHFlVjQ2Uk9BS0RRekdNTQphMXB0NVlNemVFNGZGcnhqY1l3SEhhSjN0ZEhCQkc4RWFmek5tOXk0a3Z2enZuK2JSUnNDQXdFQUFhTjhNSG93CkRnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQkJnZ3JCZ0VGQlFjREFqQVAKQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlNUVjd5K2xXQ2xNRytpRTJrTldrZjFQQWFzN3pBWgpCZ05WSFJFRUVqQVFnZzV5YjNWMFpYSXRjMmwwWlMxallUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFvQ2czCnY2a3U0YzBHYkIxV2ZXRjZnT3EzOXF2VFlmYmsxakpvdGxYTkl3dUowdTBxQ0IrTDFXYzN3MWNVM3NpZlNiSnoKY21VSWZ5Q2oyeFZRYUNGa1dDSkxtN0V0MXIrd0VYQ3V4VlJJSTRYcTdhSkVsM0w3WHFvU24rM1k4RDI1UjljLwo1OFQzNmpXcmhmVHdqWGs5Y3lDUTRxQnpkbmF5TGpqd3oyRU9BVnpFQWYyREhWZ2MrV1l2QVQxNmo5UndYMzFRCm9NWDc2azBCbHZtY2dzZGNlTTkrNmdHeGFEZnQrOUQyb05xdGNvMnhGWFBUVVRGalg4Mm05NXArbUk4UnBOMEsKRHloWFVheVo5anFKOGRORTU4TzZBUDJrelE4YWNwNHRQTGsrL3EwRjNmNnZEOGdVVHk3RWRwQiszekdFVWpwdApBcytrclNwTWV0REZRbEU0aEE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBcU9sOU1aQkFnTlZCcjFET25RRXRGMXZzc1ZBb3kzRXR2TjV3ZGJLblpBbTVTK2UrCllvT2FUdXFjVVJlSmFLMU1yMUU0UDdNeHJYV3JEY2gyakp6cGRSODNweWRNNVhKc0RtYTRHOHNGN0xLRkt0NFIKcm0zRGxoTkt5WmVNU3RqTTBwYmZoQjhldHFnalN0dUZqOS8xQ3lsNGRZa1p0bE5zVlRUN3JzaCtRWUZGVVY0SgpBcWtPa1F2bEcyb2xqeXF5UEZZbEcvaC9TSkR3OS9SSEVFajl1QWtET2Zwci84aElUSVVVVFh0MERXemZGdVcwCjg1ZzBGdGk2WW9UQmlJMjJnYzZqS1M2eUNHN1V2MTV4SXhYR3A1WGpwRTRBb05ETVl3eHJXbTNsZ3pONFRoOFcKdkdOeGpBY2RvbmUxMGNFRWJ3UnAvTTJiM0xpUysvTytmNXRGR3dJREFRQUJBb0lCQUFGckhJV3dYQlQ0NENQLwpFMkpzSW5CM2NYc05CNXFyRTZMcURKc0xGSzdFQ2lOTXRJNDlPVmJVK2krNnpvanJma3V4UWVpcHNqbHVWZHU0Ckd1UVVEcE1tTlBYRUN3MlkzV0Z0bEE3ZnNKSzJtUThDd3d2cVEySGM2RWJkd3BiVStwQ3Jld0Jhc1RaVmM4a1YKZU5TbXk4eFJNb0FYZ1BqRlVKRTlSYWtkRStVQms3dTJRc1R0QXNQNFlBUWJJMUdna0FDdGNLTUNaYnBSOXZyNQovVWc4bzJzSXBWMHJRL3F4ZVBqUGtwV3lWL25xV2hZRjI5MGN2bGVoZDBFQlVWOE1qWWduUlBuRmFsaXNaMm1iCjJzNldIZDAvMXNrVkdQS3ZsdTd0NEJVR2xMS09tSW00L09QVVRQRUJpd2liNWJycDZpVENQNkYwdWFQS09VQlIKa016VlB4RUNnWUVBMEREMmNMbyttVDhEMUIwSWxqWWx6L1UwekdKNXJaSkxzZ2F1UG5icmg2NU82RGhpUmtOTgpXSUpIQWVSNUdhV2tkOGZSS3JmRDhxUlZqOFppUGtZZDZoVjhMcHJUNHNuK1dwM1FFRE9IYlNQalYxRWdKRWYwCnU1QjFIZStjS0lVL0cvamRlVFF0RjRmNmp3bklyamhjVUs2MENMekJRcnJKbTZaRkhXaXdVZ01DZ1lFQXo3Tm0KNStWSUJpY3JsTGxQRU1aank5YUI5TkJGNFZsaEJEUDJYbVZOZnQrdE5LMUNwRWhKZVZscTJzWTdhVk5SVVpUaAp1OTJVNzQ3U2RGcHZQd1RDVkR3eER0SEhJOTB0NnRvYStqelBhMVN5cjk2aFhVejY4Ym1QaTFUTmhvQ3Z4L01yCm4xS2crNWtrNGZKV3pKd3gwUTNRdmMxTGtTM0lCdzJxWXpEeUlRa0NnWUJYalkvR1BuemU0NkpQak5vMG1aYnoKU3RLbWRXOW9jRkxIRG9vdW1NSmFjQktkRkVFMy9VdkV3aHpzamRIajJFWS9YVmY0bUFtZXZEK0RWRkd5a0xnNQoza2s0TEVLWmFJdEFQb2ZtbUZVR3NBWUdqWVp2MjVidlhrUHlqL2JqRDQ1SHpEUVBxY0tnMTcybWM5M2licTliCit1eVpsQS9PYVZFcDFSWFIxVm41VXdLQmdHY3lLZVQ2SklqNkdVc3hyemtVZVMwa0RUbkg2WkNIeWc0K2l5Qm4Ka05PQzZ4b0xJOXRnRnpGMTNnT0pEcWZNUDlFYStmVlBxTnBGeWdjSmo5QnQydWZqYURTR3dqenRmZ3o4QlA5awpDMksybUhtTlVmdDdiZ3VBT1BQdlZKYUpoYzBBNHlHcitsUkh5TzJDYk9JSWtTL2ZmMkZ1aVNjKzZlMm5Pb3RDCkhHdVJBb0dCQUpvZ280RXNBNnZBRSt6UVJ5T0pYaDJ1b2ZHaUZRSDF6VUladThSRTJCNHZmNE5IMEppZUtxVGEKQ2hvQ0luZzM2VlBQZUhxSjdWZUpXdVBXTnRobHdsV1QxM2FiQ2JrdzhKRVhjUGFpMnNYM3RhVjBLNEpMbFVkaApoeWhIeEgvQzB5clpEa2dmR2RFVlp4SjZ2emNBcUxlakVidHZuemFsSjNLRiszNzRnYk12Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + routerLocalCA: + tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURWVENDQWoyZ0F3SUJBZ0lRVEhQRTdjWHlmM042cHc4TVJzc3MwekFOQmdrcWhraUc5dzBCQVFzRkFEQWkKTVNBd0hnWURWUVFERXhka1pXWmhkV3gwTFhKdmRYUmxjaTFzYjJOaGJDMWpZVEFlRncweU5qQXpNVEV4TURRdwpNVGhhRncwek1UQXpNVEF4TURRd01UaGFNQ0l4SURBZUJnTlZCQU1URjJSbFptRjFiSFF0Y205MWRHVnlMV3h2ClkyRnNMV05oTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF5d1Rjcy9kd0RXRWcKZS9OTGVPSDBjb0NsNEs0NFhwN0lPUkc2U0ZQVG9sQXJnWm56cUxMRnRKSThzblMwMnBlcDFRbFZXRWs4aU5Qcgp4UGc1M3VJeVJTWTc1MjRIb0oxQnlKaGxGa200SzVLWmYxbVFNQVd3VUpIQ0JlTG84VnZrd0Fjd2xYSWo0NUwvCkg0eTZSUXF3azMzTzFhVzMyVXJPcGZtVlVDQ1Z2MVROSElGY0UvTTdxVnN0Z1FYN3Zib2J6cVR1ZmZLMFNkM0sKTGFHVU9pbEVwRFI0T1hlalhrS1gyazZ2QTRLY1Mwb05NbnJ2UkN4ajA3L3NZeXE1Z0F1d1BDTkdRcWoyMDROaApKazl3NkF3cnZJUHhsN2IwcmRzaS84T1FONncvaFZjOGEyNFQ0ZlRpUVpBazllZVNHUUtkMkVvM3J1RytXdk5QCnZldzJua3JFN1FJREFRQUJvNEdHTUlHRE1BNEdBMVVkRHdFQi93UUVBd0lDcERBZEJnTlZIU1VFRmpBVUJnZ3IKQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVK0hXWApkMWxQNkNkVGttNWRtUUJzanowMHJxRXdJZ1lEVlIwUkJCc3dHWUlYWkdWbVlYVnNkQzF5YjNWMFpYSXRiRzlqCllXd3RZMkV3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUtNZlNQa0tZOGZidzdLd1VmTzBhZGNvYUYxdXlyYkwKVittdEd2YWJJbEVUNTJQQWYySm1pU25aM1dobHh0TWMveXNSRXZTcjBjMXcwek1LQ2FyWnBRQWRIS3UyN1ZhaApJcHRUZlp3TzFiRjhwcHRjVkZFbDI2Qyt2RmlPNzBMK2lhMWxGYnJTdHpTSUNwTEZYTjdBSS93akcvWjltVXBHClF4MUdpNGQybSs3ZmNVY2hiRzVpdVpUckkzaXFkRnJTWWJ0SlozdnhiRGhLT2YwalNwUG9Tdk96a3FwbnA2SXoKUFNMejhqeE8rYWljalk2MFY1WS9STldCNmNRd0F3UVBFa3F5aUJ3ZytxbXNlMWQrNi9YRmVoTEhtNVhjVFcweQoxNW91VGtjeWdPenZXWjE2VHZvTGdjWFZxVFgveEhLa0pJVklaWkpUN0srZnZ4T3VDQWFCRndBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBeXdUY3MvZHdEV0VnZS9OTGVPSDBjb0NsNEs0NFhwN0lPUkc2U0ZQVG9sQXJnWm56CnFMTEZ0Skk4c25TMDJwZXAxUWxWV0VrOGlOUHJ4UGc1M3VJeVJTWTc1MjRIb0oxQnlKaGxGa200SzVLWmYxbVEKTUFXd1VKSENCZUxvOFZ2a3dBY3dsWElqNDVML0g0eTZSUXF3azMzTzFhVzMyVXJPcGZtVlVDQ1Z2MVROSElGYwpFL003cVZzdGdRWDd2Ym9ienFUdWZmSzBTZDNLTGFHVU9pbEVwRFI0T1hlalhrS1gyazZ2QTRLY1Mwb05NbnJ2ClJDeGowNy9zWXlxNWdBdXdQQ05HUXFqMjA0TmhKazl3NkF3cnZJUHhsN2IwcmRzaS84T1FONncvaFZjOGEyNFQKNGZUaVFaQWs5ZWVTR1FLZDJFbzNydUcrV3ZOUHZldzJua3JFN1FJREFRQUJBb0lCQUV4UElBdnZLaW1GUS9vRApHVyt1OHJ4bE9iUkpsL3VNMERLUFJNY3g1djhBQmxKWkJScDRVOUxMRXRCN0NJMlBhekVkcUh3ZVR3Z1pLK29sCjZVNnJFLzBrNFdoY1ZiYWIxV0dxVW5pOXJlR0c0WFphT2xXcWxicTdCc1JDcFk4dkhMekhGdzVkVURzV2dobWcKUWxWNExxWEpxSWhxbVQrdUhCMkx3Z0ZUdGlXcmRWTnBGU0xLaUJLY2d5STNRWDJ2dGhvVWdmaURTY2NIRGxKKwp0OHMzczZ1MXdROW5jd2hMSGZ6dHVYREoxemhPS2kwTUIrYncyOTZBa1piclFtSE1VVjgzdWlQditQWkcybHozCnNLeTlIMTlraGxMVkh2TmpGVWRIREJ3cFdCR2RTZUREQkwrZUViTlFneHZHWmRkQ01hbzMrRjZkdG52SDVkMkMKS2JIUjc2c0NnWUVBNjFiTUxIRndkd0lkSmJIMm85ZXhmTytLTHhtQldYMUN5Y0JkK3RIZE9odHBwanNqYTVlbgpPSjdsTnU2a0c5ZGtnLzJuMTVsSWFYdEFPM0RkU00yZDlzNUplMXFxc3dCTmtvdjFPYk1CVjFaREpBYUN4UkU2CnFRMkxYSEphMTk0NVI3V3BRVGRKYldVWEY0OTJvdUMzaFpObFNOVFhqdHFEVnRubDB5L2xCVE1DZ1lFQTNOZXQKVll3anVKeTliWVF6SkpKRGxzaUJVeWFOdTdHNk5pSkxFQ3Fyb1pQa1gwMUU2UjBUTHJoWXZSNG0yOEltK0xyTApvWnNEZFZTS09tUURFY1hNUWJBa2ZUWVhMOVV2eUlFbWdIZ0Vsb0h6SjMrNEk5SlZqUXg3bWwzNkYxWUFRUy9UCi9ZM3p6MWZyNE5SZkV2RWxuWmFib1RCVG5ldW12UWdnc0hoTXpWOENnWUVBNm1mSTlCZUZtclFiVGhtRmZjcHcKZWUycDZLSHg2YTNQWVY3ZS9ONGVDU3VXdnNFMjFZcjNQM2xjKzZzVkFMbzQzeE0vSTRzRXlqTytWYlprWW9pVApaMm92WE5PQkpNd1BlQUU1bjJBQjNQa0o1UThySDVpNm9mbmdycE1ra3RGQW9vRjU5WUJZL2NKc0RzYVJ0MGcyCjQ3QmRlUDZ2T2hYQ0xqYlpLTklTdm1zQ2dZQWRuNVN4ci8ydXF0L0NEQVNzT0M1MjBHaUFsZUJYT0F6cGJBb3oKbmZXdDA5L0RaT01FZmhEdnFHekcyWCtPNU9sRFhoTW9sMW1NYUkydUxYSTM5UmRrREZPb3RCUENKOCtrRHFieQpmcWJtNVlHUFg5TjhncDlWTDBKNVAzZm5uM0tqUzk0YzJlakZmRjY0cHVRbDcxRURaWXQwd0wzR3BqQ1VsTDJGCnptMUc4d0tCZ0hQSUFhbURGQ1hlM0ZGNjloUWp0QTRCdUNRRko0UlVmWDhScFRadTJ1cE8zTWk1d0Q0bFQyMEMKOHFicy9lTUdCdDVOL3NLMEYrY1RoK0hEd3lTdVIzWHhmZGFyTlNKVytDOUdQVmN0ZVNHbjdpOEV3enpZNEt6NApPSzFseDZpaVdhQ2hLUGV1Sm43SHIwZ0h6VzVSaEZ4QjB1NnM3N2JCL1VVbFFlUGNaNlNmCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + natsSiteCA: + tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURNakNDQWhxZ0F3SUJBZ0lRTmVmcTFGZVpOQWdTbStZSHltTnE1REFOQmdrcWhraUc5dzBCQVFzRkFEQVgKTVJVd0V3WURWUVFERXd4dVlYUnpMWE5wZEdVdFkyRXdIaGNOTWpZd016RXhNVEEwTVRNMldoY05NekV3TXpFdwpNVEEwTVRNMldqQVhNUlV3RXdZRFZRUURFd3h1WVhSekxYTnBkR1V0WTJFd2dnRWlNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDM3FobW91Um15OWFCMy95QTJQV1U3NytQUGFaZ1hYbmdpSk1mTDhEY3AKVXZmOEYwM20rSzhONzdBYzVlRTk5SFJpY1lMUUwyL2x6Q2h3VE5RNVBmaGp1NC9vaDlxMk5pSExXR21OeHBTbQo4aVRzV3FMS1VmUEVkZ2lDcmZNTTJOd1YyL1V0MjlKTXEzSzlYdTE1V0g3dnFIZUZQOGR1cWYrNEZTM1dlVFJkClZBdkovUi9KQWM5b3krTXpUWExpOXRmN1I5d2xNa2ZDQkJURUZHanpNRlpTZXZkY2hHcXk3Z284MUhCbm5qSXUKemc0NkNrUUVSbGE0eFpJdHlGVU5BRDNVNzlmejQvdVFZK3RCMGxOZytMVVk2K01pL0ZzK1BPSGlpVmw4eWMzUgpId3drMWpuNWFiV1V4NHZLZVExYVdyRFpxd3pJQ3RDMWdLalliNkdNeDBmTkFnTUJBQUdqZWpCNE1BNEdBMVVkCkR3RUIvd1FFQXdJQ3BEQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3RHdZRFZSMFQKQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVgxOXNQSVcxZFRJTG5GYnA0YW8rQUtCcUNNTXdGd1lEVlIwUgpCQkF3RG9JTWJtRjBjeTF6YVhSbExXTmhNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNaRXJ6VllzUUpDdnRlCnA4bVQ2RGlUNUhLTTJ1VmNnYkNzb2E0ck02bjRYYVp5bFAxMjZSZEJUSFhvWnNROE1pUi9rSmJHRkdBTEs5djcKTjB6ZUY3ZlFuUGV0b3NqWnVVV0pnYTF0aGdzRmZ4Yzg3TmJxSTRsN2FJVTFYa0FzN1ZObXZJRGJPYmZZVkoyVApPQkJXaWtvbkJ5aXVtOU9ncDUvT2FtRjFqVHRZS2duTGhUbzd4YlFqcksvdHJ2bWVuYXl4SWFlVWU2NHdNaFgwCmg2djJJTDQ3dnQ1b3lMOUJ4ZGcvaWorZlRLcXA1RnFMZ1dpbW5RUWxyMkhOTWlmMlljMWRKM3Nmb1EwaDZvSFMKRytpZm5aNjFHVjc1alh0ZVR6OGVPQ25oUDY5bnZKU2QvUTA3NHpvQUs1QXV1VTdFckVLYmdRQ0FEN3hxTTBlQwpqV3luNTJqcwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdDZvWnFMa1pzdldnZC84Z05qMWxPKy9qejJtWUYxNTRJaVRIeS9BM0tWTDMvQmROCjV2aXZEZSt3SE9YaFBmUjBZbkdDMEM5djVjd29jRXpVT1QzNFk3dVA2SWZhdGpZaHkxaHBqY2FVcHZJazdGcWkKeWxIenhIWUlncTN6RE5qY0ZkdjFMZHZTVEt0eXZWN3RlVmgrNzZoM2hUL0hicW4vdUJVdDFuazBYVlFMeWYwZgp5UUhQYU12ak0wMXk0dmJYKzBmY0pUSkh3Z1FVeEJSbzh6QldVbnIzWElScXN1NEtQTlJ3WjU0eUxzNE9PZ3BFCkJFWld1TVdTTGNoVkRRQTkxTy9YOCtQN2tHUHJRZEpUWVBpMUdPdmpJdnhiUGp6aDRvbFpmTW5OMFI4TUpOWTUKK1dtMWxNZUx5bmtOV2xxdzJhc015QXJRdFlDbzJHK2hqTWRIelFJREFRQUJBb0lCQUNYTHhzZUN2Zy9tdmR6cgozd1ppZUphT3piZ1ZrQ1BCQUdKd1pNQnFnUU9MVERhdjJndDk0bEp4SUxJMXVYWmRPK1UxWEZqNDVpTnBjZW40CldaVWxGRng3MFFmbWkwTURuVTFDTnNpakZOek5TSDF1UW9GMXYzOU8xZjRFaTVlNWVnTXlsb0JYTkM0c2V5cU8KNGpwZVZKTC92WWJwb3VJNmNFSkM4NEduUkRndk5jSjNLTEdMaE0xVVl3UWlubjd0QllCcDYrWjMvcExCNmRyYwp3SVhpT1A1blY3cWNjaDVwaGNaMktBVVhzYmNqcFluSEZrdE1sVlFkdWlQVHJ0UmdLcFBpMHVjRThqQkR5Ym5OCmtoMzd2elB1TUM5aFZSKzlodXBWNEoxbUNlQW05WVFqa0dobWcrM1gxZjh4d0JINFpPK1c4QXBuZUNhQ0VtdWEKbkN4b0NWa0NnWUVBN0NzTHRFZnk0VitvOE9WdDhVL1NaRE5VUWxBUkJhRzUwamhhSEtnd3hDQ3J2TWp1WHNteQpxdjd5UFRLc0s3YTBJVnBrUHliWldqdkI4ZVJybHBueWZPR2JOUDhJK3dlR0NWWmRaQXJkb1dCb25DYzU2MUs1CjNRaDFwMnJQVmd0TjdCS2Y4WEkzeVlsdkwyajI0SC9JRmhTNy81L0tjT0FOWUx3NmNoWG9LdVVDZ1lFQXh4WmYKUFZzamVJYzh0UnRzRTV2YXlBWEpHc3B6Tks2d3M1TnlTeVJhRlJiWU1Mc09GaVVVQlFiN0N1UFMwWVMyWUxEMQovZWtNcmtWWmZuWFpXSHZqeGFXLy9hL01ienhzYmdDUEJabkpoMUMyeW9lQnBMaDdUZ0p3dzJaaHdxOUl2VVVrCms4emZoclJMbUJLYldETG1RaHRIV2U1bzNHM3MzREpOUDdGN2tza0NnWUVBMmtUbWFsWmMyWUxweHNxa2svUXMKQk1PVHlqM3BuWVRkRXJkV1FVb0kyQnRCM2hidWg5aHVNcSt4L25HSXdsWDNvU1BEcHNJbSs4aGk5VWNoVUcwegp1Y3RoQU5mODJ0VVhRaVg1NW01TWE4dUlvMWwxcEZJdXlXUDZLU01FUVFmdG1wT1VFemgyNnVNRVNaTC9LSG13CjJRZU13VEpUallMbG1sUWN5RGdLL1NrQ2dZRUFnQUFLUzlDRkJjRXRidU9xb1JEYm9TN1hGYnFFUjZMcFNRdkwKdURRdkZ0QVJQNE9Fa3doVHpzZW1NR0k1OFN0NmRzQlA2R2dtRndYUGZGY1kzcU1JMXRLeWxkQ3BoL3M1VzZCUQpWREdFT05QVU1uTGRENkxzNUVMOWJTUXVScFdjRnRTVnA5RlpCYXAxejlobXVGWkJaTTlWR0tVSUZuRTJrSHhtCjNrU21Sc0VDZ1lFQW5yK01ZR0MzOFNuZVQ3dExSZm1QVEdEQXk4VWs2dm51ei9ySUN2YU4vSlNDbUFyS0wvSG8KQ091VHpOUUs4Y2twYkdQZGN1NnZWS1Y4bWJ6d0ZETCs4czRMZjhmRlBSajc2YVFiNEp3MTNTVHZlU2hSa2xaLwo1aGY4N3ZWWVd2SXkwQWovbExxdGdSY1p6RXpzWWdQQ0pGenlsU1Azb3BSVFg2emtYZ2V6NVpNPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= + natsLocalCA: + tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVRENDQWppZ0F3SUJBZ0lSQVA4SlI0R0lYZm4wOUpKaWsrMkVIaDB3RFFZSktvWklodmNOQVFFTEJRQXcKSURFZU1Cd0dBMVVFQXhNVlpHVm1ZWFZzZEMxdVlYUnpMV3h2WTJGc0xXTmhNQjRYRFRJMk1ETXhNVEV3TkRFegpObG9YRFRNeE1ETXhNREV3TkRFek5sb3dJREVlTUJ3R0ExVUVBeE1WWkdWbVlYVnNkQzF1WVhSekxXeHZZMkZzCkxXTmhNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXNGSW1NZFJvU1VROFpOcTkKbDVRNnB0RWVub2hxMTdQWHIxMHdPVlZvd2J3VHBrUDZBdFgrMnhWMDNUbzF4SEoxdWZoOXczbWdKd2JaQW5iRAp3amdIU2ZmZkIyRjM2bWZjeVF4RGpZdTY2cGV6SVNzMlJNNVFwZEZhcTdoN0k5clIyNUorcGRPWnpzM29vUEt1CmFWWHpFUVVVUE1QNkxBem1pZndMVnAyUGFlZGZaRENxR1laL202eFA4cGtSSmdXc21RWmdWZVJ6WjdBMzNXSDkKRVFmTnc2WHZmN3NmQ0lvRUFVeFozaVZLd3hoMjJhaVpsRW03aTdYZkdLNVVBRFhVbG54V214UVFlM005dWNXMgprY21vbGNnKzA4dlc5bzFhSzdkNS91VmVNNExSbGVKSVF2R3c2YkFmVElNU0Q5L0gzdmg2M1diVis0N1krT2xtCkx6ZXJRUUlEQVFBQm80R0VNSUdCTUE0R0ExVWREd0VCL3dRRUF3SUNwREFkQmdOVkhTVUVGakFVQmdnckJnRUYKQlFjREFRWUlLd1lCQlFVSEF3SXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVNcjdyYmJ5VApBanNmVnVSQmI5Mm5SRDcvYkZVd0lBWURWUjBSQkJrd0Y0SVZaR1ZtWVhWc2RDMXVZWFJ6TFd4dlkyRnNMV05oCk1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ0hmbmdaeXJwMFJtaytMaG9qdkZrbVNyWlBkc0FFakpDWTNSSVgKTEVYZ2FzNTk5V3lERXJIaEo3MXNDTTdneWJGVXdKV3ZyOXliY2NZdVJqa2V5RE11bC9USlZjUERwc29zOGhrNgpOaElCa3pwRlZLdDRmL2t5NUpNMVJHdWxKRXdwNGNqazRMblJ0VFV5UGJpSm5XR3oybXBOaWFHVlI5bTluUW5QCk1mS0Q4dDJnZjdBV3pGS1poTWVRclZYMldmWHowKzRRUjJXck5NSjVnb0JHR1NCYTNrYU5LdE1ZS29BV25WYUEKME9FcGQ5VFNKZFlNUURwcVpTSVFhMEQya0Uvbi9PanNXTktyNGlzajVteEY2OU9vaHZhYm9YUFlORVI3YWl6OQpTc1B1Rmc2bjZLaXVYdkZnMGdFdE5Mb1dCZlBNaTZJOEhtY0MvdWlyNTlRU2JXZEYKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBc0ZJbU1kUm9TVVE4Wk5xOWw1UTZwdEVlbm9ocTE3UFhyMTB3T1ZWb3did1Rwa1A2CkF0WCsyeFYwM1RvMXhISjF1Zmg5dzNtZ0p3YlpBbmJEd2pnSFNmZmZCMkYzNm1mY3lReERqWXU2NnBleklTczIKUk01UXBkRmFxN2g3STlyUjI1SitwZE9aenMzb29QS3VhVlh6RVFVVVBNUDZMQXptaWZ3TFZwMlBhZWRmWkRDcQpHWVovbTZ4UDhwa1JKZ1dzbVFaZ1ZlUnpaN0EzM1dIOUVRZk53Nlh2ZjdzZkNJb0VBVXhaM2lWS3d4aDIyYWlaCmxFbTdpN1hmR0s1VUFEWFVsbnhXbXhRUWUzTTl1Y1cya2Ntb2xjZyswOHZXOW8xYUs3ZDUvdVZlTTRMUmxlSkkKUXZHdzZiQWZUSU1TRDkvSDN2aDYzV2JWKzQ3WStPbG1MemVyUVFJREFRQUJBb0lCQUE0cEJzcTlMNFBrMEVIYwpSRm9xVEJ5T0VscXlnM3dmeEJ4Z0RFbXFkNCtKaW4xeG02QkRMZVRMNEJjTlAvaXZKWS9DS2wxNnhOY2xpR09YCmhLaXlKYm0xeDZwWTFFL1Z1QWhJYlJ0dXc1dm9BM21RTmh0SUEzZVJyT202RnQrZUNQa01scEc4UU8rNEh5emYKMkl4Nm05cjcwTENCbjdPT2RLeFR1dGhocm4wWGRzODZIR21TTXpWWGFJZDBHeDA5LzV0c3BtZFBpWEpUWnluTQowR2lULzVHVHdWQUlLcEU4cjNRRjJnbWRqN2pBK3ZxT0V2eFNyYlcwRjVlT0hjRmRuMHBMUlNLWEVBRVVhaWNYCndGRVR4eWcrMHFtR3orcGZPWFQzbVFDU2kwbCtQSlRCdG1kVVZoQWg3cTdFODlEaWNHeW52UndGVXVKVGxheDUKTm5kUVVMMENnWUVBd2J3dUwwcFhnWXRuMDBLUThxN2gweGhnOVJZd0ZmTnREYzg2YSs3clRzcGlJTmZ5MDdQQwpyakhZS0dVejNZY1lFL1EzZGUxczQyUGVCM0NWaTJmeVBEZzJKSE1ySHpKOXFCY0pVam9GUzFQOHVRdlBjQlpxCmp2S1BYN0M1OFpKbXRkT0hsUXN0RG1OS3l3YjQ4czEveXhvQVh2M1ptN3V5RDFPZW9wWjYvQlVDZ1lFQTZQMHgKWDR6bEpRV0FIT0FPYWh0TnlLMVlYdWlZUUZOelRpOHVnQUZlbWdOdFJ3a1dCdWFNMENQTnZDZ0tDS3QxNEFHdgp3cHdMYlVuOFBBUXZWa2dRWlJneHhWdGZhU1kxNlIvYVJlRDJHVHZwSDc1YlRMWDd1SG5JaENMSUFDWnFtS1A2CkhvYWpFU0hHRi9ZaEtyV21SS0VReDhhVUpxMVRqV1FmQVFDVWdYMENnWUJXdUgyVC9ac2VDZUQzMkJ3NkJiNWcKVjlGTzVCZXlPN3pkS1ozbElwV0NOMldsZmdUY2J1TCtScUdUczNsNytEVDIrYUs1endXbTQ5VkhUMFlobU8zOQp0c3ZGbFNnQVZ3R1lkSGRmcjBrZlp3RUJkQi91OUpuT1V4V0twL2tVQVl5b1ozK1JYK2RUUVc4QllxV2RTZytpClFvbFgvQm1rZEdoSUpBNG1pV1dUNFFLQmdIWk1lTkZIUE9IN1ZQMVVWbjFSdDhENUl6R3RjQURaWG1hSVZsZncKV2hSaFFROGNjZTYzQ1RCMXZYU1g3K0JQRHQ3YWZGK1gwOFYrRjNCeHY0ZFR0OTljMVlpYnlHb2ZXS2d4NENZeQovMEg0eFhtMHNhN1ZpQ1kyejdVbjQ5MFBwSGcwYWo4dHBZYUJXNCszRFVnZVMzbjFQZ3Z4ckMrbk9oRkVrT2wxClhmSVJBb0dCQUlvb0ZyZ1pKelFHR3NEcGJ4VXJUVXBLK2xVekF0c0FFcWN6WnJWRm4rVlRFZ0dvMGRsdjRTVE0KK1lhRTdCUHpMbE1IeWZQdzVzMUpCbHVLVXIvcEpzOGZLWnlEaVVOcWR6S2NDZkJqNVJzd0NHV3IxbFZUN3RqQwpUeURiVFg3UmxWWVp0Y0FEVGxFa3kyYW9admN0OTIzbmEzM0R5V2VNUjBJUkg2S05VcXdLCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + tls: + ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlEUXpDQ0FpdWdBd0lCQWdJV0JBQ0RFWkpsWjVpRFpUT0lFUUlZT0RFakFaZ09OekFOQmdrcWhraUc5dzBCDQpBUXNGQURBYU1SZ3dGZ1lEVlFRREV3OTBaWE4wTFdOdmJuUnliMnhzWlhJd0hoY05Nall3TmpJek1Ua3hNekV4DQpXaGNOTWprd05qSXpNVFV5TWpRM1dqQWFNUmd3RmdZRFZRUURFdzkwWlhOMExXTnZiblJ5YjJ4c1pYSXdnZ0VpDQpNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNhV0R3aE42d2M3dUh0ZHZWZXB4T2FJMUNaDQo1d1NBWlJtbTJzMUlMajh3NUwxWllYT1BjTGFJNzhkRTM2dnlDN0VmM3JrL0dhV0cwMHJlampZWGtWakZxV0pLDQpGZENoelRadFkxK0tPSWM4UWJiVlpqanFmZjBPSHVDNnB3d3ZpQXZYNWQ0VmJmOXVPek8wS2pCMzdFR01KUHE2DQo4YWJuOU5mOXJVRHFDcm5OVlhJWEJQQllnd0hPOTlrdWZoNERmZ0R0V0dnYjVwa1J0Y2ZpOWkzdEd2YkFSaFk5DQo3TlQ3b1I2NVpBK3BNMndEQU1mSHVpNTZySTNkZUR1Ly9kQXdOUHM0UmFncFl4OWxld080MTByYmVESmlPbW1NDQo4SXBaNXZsaVdGdGxoa1BwVHV4M3JJTFB3Ym1ERDl1TkZRKzQwbjR0L3g3WEJzcjZsV2EzMjlndVRRWWJBZ01CDQpBQUdqZnpCOU1Bd0dBMVVkRXdFQi93UUNNQUF3RGdZRFZSMFBBUUgvQkFRREFnV2dNQjBHQTFVZEpRUVdNQlFHDQpDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFmQmdOVkhSRUVHREFXaHdUQXFJdFZnZzR4T1RJdU1UWTRMakV6DQpPUzQ0TlRBZEJnTlZIUTRFRmdRVVdoaVB6RE5VR3YwZzQ0V3pOY0FjdzFMVlBsQXdEUVlKS29aSWh2Y05BUUVMDQpCUUFEZ2dFQkFGTHN0amZmMFlPQmlGLy8vbkpVUXNDUld0dmxXVXBQQzBxOWE5d0ROL1pXUVMyZTZhMzVmYXJiDQp4N0dkUmFPVTMyUS9HTWpEUVJ3amVDZTlBNXZEZEpPN3ZFZWtPbjBnVEhScnVCTVYzbHNkVzZCZFhNQ3dtRG5TDQpBZlZja091TGpFRjYzQ1RsS1R6Vnp5QmFkZW1IVnFab2djTDdNSHNLZHFjZkNWVjlpYnlrcklzbVdCNWx2aG5iDQpJNjlId3l4Vk5OOHZWSEtuRmRqdVJHR0sybk5ubGNNTXIrR2NsNWFRUGlSTS82ME1EbzkyUXg4dzhjSTBGdzgrDQpqY0VvS3pTTzZ0VVk3NjlUVEF0eVowOFlCRllQbEk3RkkvbVkvckxNbU9Md21wL3hXSVFYeGlSNUNwbS9sUVBwDQozZldGUFlId2hOdjBsYzU0UjZQV2tNaDlKT0hnZWFNPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0K + cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlEUXpDQ0FpdWdBd0lCQWdJV0JBQ0RFWkpsWjVpRFpUT0lFUUlZT0RFakFaZ09OekFOQmdrcWhraUc5dzBCDQpBUXNGQURBYU1SZ3dGZ1lEVlFRREV3OTBaWE4wTFdOdmJuUnliMnhzWlhJd0hoY05Nall3TmpJek1Ua3hNekV4DQpXaGNOTWprd05qSXpNVFV5TWpRM1dqQWFNUmd3RmdZRFZRUURFdzkwWlhOMExXTnZiblJ5YjJ4c1pYSXdnZ0VpDQpNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNhV0R3aE42d2M3dUh0ZHZWZXB4T2FJMUNaDQo1d1NBWlJtbTJzMUlMajh3NUwxWllYT1BjTGFJNzhkRTM2dnlDN0VmM3JrL0dhV0cwMHJlampZWGtWakZxV0pLDQpGZENoelRadFkxK0tPSWM4UWJiVlpqanFmZjBPSHVDNnB3d3ZpQXZYNWQ0VmJmOXVPek8wS2pCMzdFR01KUHE2DQo4YWJuOU5mOXJVRHFDcm5OVlhJWEJQQllnd0hPOTlrdWZoNERmZ0R0V0dnYjVwa1J0Y2ZpOWkzdEd2YkFSaFk5DQo3TlQ3b1I2NVpBK3BNMndEQU1mSHVpNTZySTNkZUR1Ly9kQXdOUHM0UmFncFl4OWxld080MTByYmVESmlPbW1NDQo4SXBaNXZsaVdGdGxoa1BwVHV4M3JJTFB3Ym1ERDl1TkZRKzQwbjR0L3g3WEJzcjZsV2EzMjlndVRRWWJBZ01CDQpBQUdqZnpCOU1Bd0dBMVVkRXdFQi93UUNNQUF3RGdZRFZSMFBBUUgvQkFRREFnV2dNQjBHQTFVZEpRUVdNQlFHDQpDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFmQmdOVkhSRUVHREFXaHdUQXFJdFZnZzR4T1RJdU1UWTRMakV6DQpPUzQ0TlRBZEJnTlZIUTRFRmdRVVdoaVB6RE5VR3YwZzQ0V3pOY0FjdzFMVlBsQXdEUVlKS29aSWh2Y05BUUVMDQpCUUFEZ2dFQkFGTHN0amZmMFlPQmlGLy8vbkpVUXNDUld0dmxXVXBQQzBxOWE5d0ROL1pXUVMyZTZhMzVmYXJiDQp4N0dkUmFPVTMyUS9HTWpEUVJ3amVDZTlBNXZEZEpPN3ZFZWtPbjBnVEhScnVCTVYzbHNkVzZCZFhNQ3dtRG5TDQpBZlZja091TGpFRjYzQ1RsS1R6Vnp5QmFkZW1IVnFab2djTDdNSHNLZHFjZkNWVjlpYnlrcklzbVdCNWx2aG5iDQpJNjlId3l4Vk5OOHZWSEtuRmRqdVJHR0sybk5ubGNNTXIrR2NsNWFRUGlSTS82ME1EbzkyUXg4dzhjSTBGdzgrDQpqY0VvS3pTTzZ0VVk3NjlUVEF0eVowOFlCRllQbEk3RkkvbVkvckxNbU9Md21wL3hXSVFYeGlSNUNwbS9sUVBwDQozZldGUFlId2hOdjBsYzU0UjZQV2tNaDlKT0hnZWFNPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0K + key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ0KTUlJRW93SUJBQUtDQVFFQW1sZzhJVGVzSE83aDdYYjFYcWNUbWlOUW1lY0VnR1VacHRyTlNDNC9NT1M5V1dGeg0KajNDMmlPL0hSTityOGd1eEg5NjVQeG1saHROSzNvNDJGNUZZeGFsaVNoWFFvYzAyYldOZmlqaUhQRUcyMVdZNA0KNm4zOURoN2d1cWNNTDRnTDErWGVGVzMvYmpzenRDb3dkK3hCakNUNnV2R201L1RYL2ExQTZncTV6VlZ5RndUdw0KV0lNQnp2ZlpMbjRlQTM0QTdWaG9HK2FaRWJYSDR2WXQ3UnIyd0VZV1BlelUrNkVldVdRUHFUTnNBd0RIeDdvdQ0KZXF5TjNYZzd2LzNRTURUN09FV29LV01mWlhzRHVOZEsyM2d5WWpwcGpQQ0tXZWI1WWxoYlpZWkQ2VTdzZDZ5Qw0KejhHNWd3L2JqUlVQdU5KK0xmOGUxd2JLK3BWbXQ5dllMazBHR3dJREFRQUJBb0lCQUFKZC94SVhpSXBmYkhiUw0KSDc5ZGtmNUI0Zkg5OWRRSVNsK1FFWnp1dnRRdU1CcmtnMm44Uncrdk96Y2R0RnlpZkVWMS95aUZYWld1TU91Sw0KejlMdktqNG5xRGZDQnhPSlRHdUpXRkRSK1RmcGdURithQmNuZ2ljRkp6ZExkV2VkQ1drWnJub25CckFhRWJVUw0KaWpabUdNNjNxNytBaG5zMW5iYjZRU0pORDRvQWdXcklEYnBvdytCNitQY0lWMXcrQitpN3VvK2p1c0E1MVJKcQ0KMC8yLzcwQUhubXA2RlFaZi9lUldKR0hXZjNiUzNEck94VFlLdUNFYU1Tc04xcWczZ2lhNEgvU1dLL2E5ZGMxcg0KQmJvRllYVE5sZ2ZUTlM3SDNjTnM5OTNTdHdNbDIvd2J6QlV3c1hCV1hsZ2lnRnhhT1B4OTVTV2J6azZINDJVcw0KSFJVRmxjMENnWUVBMVhrb3lxWVYzZHlrUGFWMU80RkV3b3VPR3V5TVhYcHFqYTk3dmRQR3RtU3RhaERkYmx4SQ0KNzR3U3ZYb2Z4ZWNycnF0TlZ3NjdBS2VNNFY0aXhySzNhS1Y3UUdWL2RsZnlMN2tLRFlpZnBnTkdja2V0Tk80aw0KeUE5NmFQcFEzeGZCUWMvK091eFdZSDRJY3VVVC84WnFiSThPemhySzQ0U0x6S1NPTzZ0dDlhMENnWUVBdVJlWg0KTi85NDZTU1M3NEJTQWpWR3lPblJoUnlkMHM3cFI4WnRHZmhhbFlEZlplb2lkTHZwZWRLQzZEYkxXdkJrOEdZSw0KNjFYZllPcWlTclBMa3c1VlVZdTlUVmJUWHRJeTlvZUhFZUJEM3pudVRRVW9yb0lUOHZjek1iMWNWWW1MQkRYag0KbWdHclFRdzFtY2ExN2VuT2l0V1R3ZWo5b1dHaXVhZEE4MjVLaytjQ2dZQUw2SjVsNzYzNG1uNXZFZlBnUmUzWQ0KRElENDc0bEZEYmMwQUQ0ZThObDlBMURKUWZlWVdIMlpIMjlNTXF2akZtcFJiQ3o4Vms0SVUxQ0FvZ2UrbmVtdA0KWk0zalljWWlpL1Z1eEJ2VGRYT3loeXcrNDlDOXl5c3lIZXJ1UUVpU3FYaVdlMHZyYlpQRC9rUHFaTzBncjZqdw0KTldyV0JKaWM2S0FENG9vc3VmdUFZUUtCZ0JWYlhiaVNaOWN1K3kxYmR4citIcjdNQy9yNkJGUHd3QVlpSlRDYg0KOFlmU3FQUlBnYzVLYUhSQUVBN1BVOE9ZZlcwbnVSYlNmOFhsRFBqbHFoVzd6NmhySVZxdExCS0MycEtMck5BcA0KT211bGVaTzFocTRzSURVbXhPZDJYQk1hbmNuTWxnaU5MTCtDc3lTZFF4ekNuNnh4WEcxQmZ4S0IrNDdFZDhBZQ0KOThmOUFvR0JBS05oU29Ud1dxTE9SOXV1TkJNU2tIRlN6bm1JVXV1dXF4Zy9qMzFRRW1uRzJUbzhaR1kvQmN3Rw0KOXVXenhCY3dvUVBVUTFKeEdsTktrcXhUNnBYVDZIdVVBVHNaMnRIb2M0dkt1c1d3S2tOcFVycmRpMjdNMFhZSg0KNktiL3lMdTkva1ZPdjErazdscEFseXd4TVpjZ21MOVhsZWRjb3V4WmQzKzY1N1FsS2ZZUQ0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0NCg== + nats: + enabled: true + events: + auditEnabled: true + retentionDays: 14 + cleanupInterval: 86400 + captureIpAddress: true + controllers: + - name: remote-1 + host: 0.0.0.0 + ssh: + user: ubuntu + keyFile: /Users/emirhan/.orbstack/ssh/id_ed25519 + port: 32222 + systemAgent: + config: + host: 192.168.139.85 + arch: arm64 + containerEngine: edgelet + deploymentType: native + # - 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/yaml.go b/internal/resource/yaml.go index e0c81b499..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,10 +34,8 @@ 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 } From 3256e5d8963aef22d4edb3591bcf9b6538422674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Wed, 24 Jun 2026 01:06:02 +0300 Subject: [PATCH 34/63] Centralize system and edge agent defaults for deploy paths. Extract shared AgentConfiguration defaults and validation so local and remote system agents use the same preparation logic. --- internal/deploy/agentconfig/defaults.go | 179 ++++++++++++++++++ internal/deploy/agentconfig/utils.go | 49 ++--- internal/deploy/agentconfig/utils_test.go | 83 ++++++++ .../deploy/controlplane/local/system_agent.go | 90 ++------- 4 files changed, 296 insertions(+), 105 deletions(-) create mode 100644 internal/deploy/agentconfig/defaults.go create mode 100644 internal/deploy/agentconfig/utils_test.go 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/utils.go b/internal/deploy/agentconfig/utils.go index cd70be280..af4746d26 100644 --- a/internal/deploy/agentconfig/utils.go +++ b/internal/deploy/agentconfig/utils.go @@ -5,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" ) @@ -37,31 +38,13 @@ func getNatsMode(config *rsc.AgentConfiguration) NatsMode { } 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 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 config == nil { + return nil } - 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)) + if iutil.IsSystemAgent(config) { + return validateSystemAgent(config) } - - return nil + return validateEdgeAgent(config) } func findAgentUUIDInList(list []client.AgentInfo, name string) (uuid string, err error) { @@ -125,26 +108,27 @@ func Process(agentConfig *rsc.AgentConfiguration, name, agentIP string, otherAge } func getAgentUpdateRequestFromAgentConfig(agentConfig *rsc.AgentConfiguration, tags *[]string) (request client.AgentUpdateRequest) { - var archPtr *int64 - if agentConfig.Arch != nil { - arch, found := rsc.ArchStringToID(*agentConfig.Arch) - if !found { - arch = 0 - } - archPtr = &arch - } request.Location = agentConfig.Location request.Latitude = agentConfig.Latitude request.Longitude = agentConfig.Longitude request.Description = agentConfig.Description request.Name = agentConfig.Name - request.ArchID = archPtr 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, @@ -165,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/controlplane/local/system_agent.go b/internal/deploy/controlplane/local/system_agent.go index 5f36efeac..e4ce6773f 100644 --- a/internal/deploy/controlplane/local/system_agent.go +++ b/internal/deploy/controlplane/local/system_agent.go @@ -10,7 +10,6 @@ import ( 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/pkg/util" ) @@ -18,41 +17,8 @@ import ( const ( deploymentTypeNative = "native" deploymentTypeContainer = "container" - - defaultNatsServerPort = 4222 - defaultNatsClusterPort = 6222 - defaultNatsLeafPort = 7422 - defaultNatsMqttPort = 8883 - defaultNatsHTTPPort = 8222 - defaultJsStorageSize = "10G" - defaultJsMemoryStoreSize = "1G" ) -func applyLocalSystemAgentNatsDefaults(cfg *rsc.AgentConfiguration) { - 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) - } -} - func buildLocalSystemAgentConfig(cp *rsc.LocalControlPlane, name string) (rsc.AgentConfiguration, error) { sys := cp.SystemAgent if sys == nil { @@ -84,14 +50,6 @@ func buildLocalSystemAgentConfig(cp *rsc.LocalControlPlane, name string) (rsc.Ag deployAgentConfig.DeploymentType = iutil.MakeStrPtr(deploymentType) } - if deployAgentConfig.RouterMode == nil { - interior := iofog.RouterModeInterior - deployAgentConfig.RouterMode = &interior - } else if *deployAgentConfig.RouterMode != iofog.RouterModeInterior { - interior := iofog.RouterModeInterior - deployAgentConfig.RouterMode = &interior - } - if deployAgentConfig.UpstreamRouters == nil { emptyRouters := []string{} deployAgentConfig.UpstreamRouters = &emptyRouters @@ -101,20 +59,7 @@ func buildLocalSystemAgentConfig(cp *rsc.LocalControlPlane, name string) (rsc.Ag deployAgentConfig.UpstreamNatsServers = &emptyNats } - if deployAgentConfig.EdgeRouterPort == nil { - edgeRouterPort := 45671 - deployAgentConfig.EdgeRouterPort = &edgeRouterPort - } - if deployAgentConfig.InterRouterPort == nil { - interRouterPort := 55671 - deployAgentConfig.InterRouterPort = &interRouterPort - } - if deployAgentConfig.MessagingPort == nil { - messagingPort := 5671 - deployAgentConfig.MessagingPort = &messagingPort - } - - applyLocalSystemAgentNatsDefaults(&deployAgentConfig) + deployagentconfig.ApplySystemAgentDefaults(&deployAgentConfig) if deployAgentConfig.Name == "" { deployAgentConfig.Name = name @@ -147,36 +92,35 @@ func deployLocalSystemAgent(namespace string, cp *rsc.LocalControlPlane, name st if _, err := edgelet.Configure(endpoint, user); err != nil { return fmt.Errorf("failed to provision system agent: %w", err) } - return persistLocalSystemAgent(namespace, cp, name, deployAgentConfig, endpoint) + return persistLocalSystemAgent(namespace, cp, name, deployAgentConfig, endpoint, configExe.GetAgentUUID()) } -func persistLocalSystemAgent(namespace string, cp *rsc.LocalControlPlane, name string, deployAgentConfig rsc.AgentConfiguration, endpoint string) error { +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 } - 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 := 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 - host := agentInfo.Host - if deployAgentConfig.Host != nil && strings.TrimSpace(*deployAgentConfig.Host) != "" { - host = *deployAgentConfig.Host - } - agent := &rsc.LocalAgent{ Name: name, - UUID: agentInfo.UUID, + UUID: uuid, Created: util.NowUTC(), - Host: host, + Host: *host, ControllerEndpoint: endpoint, Airgap: cp.Airgap, Config: &configCopy, From b89e6d647c9428c6d5e33358b1a41f8675dd96f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Wed, 24 Jun 2026 01:06:08 +0300 Subject: [PATCH 35/63] Add remote edgelet manifest deploy helpers and tune default status polling. Support writing manifests on remote hosts and applying them via edgelet deploy -f, and reuse the helper from local control plane deploy. --- assets/edgelet/edgelet-config.yaml | 2 +- .../deploy/controlplane/local/edgelet_host.go | 4 +- pkg/iofog/install/edgelet_config_test.go | 2 +- pkg/iofog/install/edgelet_deploy.go | 19 ++++- pkg/iofog/install/edgelet_deploy_test.go | 43 +++++++++++ pkg/iofog/install/edgelet_remote.go | 77 ++++++++++++++++++- pkg/iofog/install/paths.go | 5 +- pkg/iofog/install/procedures_test.go | 4 +- 8 files changed, 146 insertions(+), 10 deletions(-) diff --git a/assets/edgelet/edgelet-config.yaml b/assets/edgelet/edgelet-config.yaml index 7518a57af..e3da23ada 100644 --- a/assets/edgelet/edgelet-config.yaml +++ b/assets/edgelet/edgelet-config.yaml @@ -34,7 +34,7 @@ profiles: logDiskDirectory: "/var/log/edgelet/" logFileCount: "10" logLevel: "INFO" - statusFrequency: "30" + statusFrequency: "10" changeFrequency: "10" scanDevicesFreq: "60" gps: "auto" diff --git a/internal/deploy/controlplane/local/edgelet_host.go b/internal/deploy/controlplane/local/edgelet_host.go index c4ad89150..088d25045 100644 --- a/internal/deploy/controlplane/local/edgelet_host.go +++ b/internal/deploy/controlplane/local/edgelet_host.go @@ -142,7 +142,7 @@ func deployPrivateEdgeletRegistry(cp *rsc.LocalControlPlane, edgelet *install.Lo return nil, util.NewError("private registry translation produced no manifest") } - path, cleanup, err := install.WriteTempManifest(result.Registry, "edgelet-registry") + path, cleanup, err := edgelet.WriteDeployManifest(result.Registry, "edgelet-registry") if err != nil { return nil, err } @@ -172,7 +172,7 @@ func deployEdgeletControlPlane(cp *rsc.LocalControlPlane, edgelet *install.Local return err } - path, cleanup, err := install.WriteTempManifest(result.ControlPlane, "edgelet-controlplane") + path, cleanup, err := edgelet.WriteDeployManifest(result.ControlPlane, "edgelet-controlplane") if err != nil { return err } diff --git a/pkg/iofog/install/edgelet_config_test.go b/pkg/iofog/install/edgelet_config_test.go index b5457014f..8133e1e8b 100644 --- a/pkg/iofog/install/edgelet_config_test.go +++ b/pkg/iofog/install/edgelet_config_test.go @@ -336,7 +336,7 @@ func TestBootstrapEnvDesktopContainer(t *testing.T) { 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/potctl-edgelet-scripts/bin:") { + 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 index d17e0686c..faf682fe8 100644 --- a/pkg/iofog/install/edgelet_deploy.go +++ b/pkg/iofog/install/edgelet_deploy.go @@ -35,8 +35,18 @@ func (agent *LocalEdgelet) RegistryList() (string, error) { } // WriteTempManifest writes YAML to a temp file for edgelet deploy -f. -func WriteTempManifest(data []byte, prefix string) (path string, cleanup func(), err error) { - f, err := os.CreateTemp("", prefix+"-*.yaml") +// 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, 0o755); err != nil { + return "", nil, err + } + } + + f, err := os.CreateTemp(dir, prefix+"-*.yaml") if err != nil { return "", nil, err } @@ -53,6 +63,11 @@ func WriteTempManifest(data []byte, prefix string) (path string, cleanup func(), return path, func() { _ = 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) diff --git a/pkg/iofog/install/edgelet_deploy_test.go b/pkg/iofog/install/edgelet_deploy_test.go index 88ef10eaf..0c8fe807b 100644 --- a/pkg/iofog/install/edgelet_deploy_test.go +++ b/pkg/iofog/install/edgelet_deploy_test.go @@ -1,11 +1,54 @@ 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 diff --git a/pkg/iofog/install/edgelet_remote.go b/pkg/iofog/install/edgelet_remote.go index 58cbccad7..1d408cf76 100644 --- a/pkg/iofog/install/edgelet_remote.go +++ b/pkg/iofog/install/edgelet_remote.go @@ -10,6 +10,8 @@ import ( "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 @@ -317,6 +319,79 @@ func (agent *RemoteEdgelet) installRemoteFileIfMissing(destPath string, content 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 := os.ReadFile(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", @@ -410,7 +485,7 @@ func (agent *RemoteEdgelet) copyInstallScripts() error { func edgeletScriptTmpName(relPath string) string { safe := strings.ReplaceAll(relPath, "/", "-") - return "potctl-edgelet-" + safe + ".tmp" + return "edgelet-" + safe + ".tmp" } func (agent *RemoteEdgelet) installRemoteScript(stageDir, relPath, content string) error { diff --git a/pkg/iofog/install/paths.go b/pkg/iofog/install/paths.go index 7f1e7fcaa..51d3c9bf3 100644 --- a/pkg/iofog/install/paths.go +++ b/pkg/iofog/install/paths.go @@ -15,7 +15,10 @@ func EdgeletShareDir(hostOS string) string { } // EdgeletScriptStageDir is the transient directory used before publishing to EdgeletShareDir. -const EdgeletScriptStageDir = "/tmp/potctl-edgelet-scripts" +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)) { diff --git a/pkg/iofog/install/procedures_test.go b/pkg/iofog/install/procedures_test.go index 1699efa6c..4ce2141e2 100644 --- a/pkg/iofog/install/procedures_test.go +++ b/pkg/iofog/install/procedures_test.go @@ -4,11 +4,11 @@ import "testing" func TestEntrypointGetCommandQuotesInstallArgs(t *testing.T) { ep := Entrypoint{ - destPath: "/tmp/potctl-edgelet-scripts/install.sh", + destPath: "/tmp/edgelet-scripts/install.sh", Args: []string{"--version=v1.0.0-rc.3", "--arch=arm64", "--skip-start"}, } got := ep.getCommand() - want := "/tmp/potctl-edgelet-scripts/install.sh '--version=v1.0.0-rc.3' '--arch=arm64' '--skip-start'" + want := "/tmp/edgelet-scripts/install.sh '--version=v1.0.0-rc.3' '--arch=arm64' '--skip-start'" if got != want { t.Fatalf("getCommand() = %q, want %q", got, want) } From 399a7c15b0fe1fa6ee07b8467750272b5c1a1d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Wed, 24 Jun 2026 01:06:17 +0300 Subject: [PATCH 36/63] Deploy router and NATS global certificates once via Controller API. Replace per-controller router cert install with DeployGlobalCertificates and fix TLS secret key names for the controller API. --- pkg/iofog/install/controller.go | 150 ++++++++++++++---- .../install/controller_certificates_test.go | 117 ++++++++++++++ 2 files changed, 239 insertions(+), 28 deletions(-) create mode 100644 pkg/iofog/install/controller_certificates_test.go diff --git a/pkg/iofog/install/controller.go b/pkg/iofog/install/controller.go index 853ebbbd3..9ac118e7b 100644 --- a/pkg/iofog/install/controller.go +++ b/pkg/iofog/install/controller.go @@ -1,6 +1,7 @@ package install import ( + "errors" "fmt" "os" "path/filepath" @@ -770,27 +771,6 @@ func (ctrl *Controller) waitForControllerToStart() (string, error) { 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 - } - } - // 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 { - 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") @@ -827,11 +807,6 @@ func (ctrl *Controller) Install() (err error) { return } - // Deploy router certificates - if err = ctrl.deployRouterCertificates(endpoint); err != nil { - return - } - return nil } @@ -892,8 +867,9 @@ func DeployRouterSecrets(endpoint, secretName string, TLSCert, TLSKey string) (e Name: secretName, Type: "tls", Data: map[string]string{ - "TLSCert": TLSCert, - "TLSKey": TLSKey, + "ca.crt": TLSCert, + "tls.crt": TLSCert, + "tls.key": TLSKey, }, } @@ -923,3 +899,121 @@ func ImportRouterCertificate(endpoint string, secretName string) (err error) { 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 (g GlobalCertificates) empty() bool { + return g.RouterSiteCA == nil && g.RouterLocalCA == nil && g.NatsSiteCA == nil && g.NatsLocalCA == nil +} + +// 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 + } + 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 _, pair := range pairs { + if err := deployGlobalCertificate(clt, pair.name, pair.cert); err != nil { + return err + } + } + return nil +} + +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 isControllerNotFound(err error) bool { + var notFound *client.NotFoundError + return errors.As(err, ¬Found) +} + +func isControllerConflict(err error) bool { + var conflict *client.ConflictError + return errors.As(err, &conflict) +} + +func controllerSecretExists(clt globalCertDeployer, name string) (bool, error) { + _, err := clt.GetSecret(name) + if err == nil { + return true, nil + } + if isControllerNotFound(err) { + return false, nil + } + return false, err +} + +func controllerCAExists(clt globalCertDeployer, name string) (bool, error) { + _, err := clt.GetCA(name) + if err == nil { + return true, nil + } + if isControllerNotFound(err) { + return false, nil + } + return false, err +} + +func deployGlobalCertificate(clt globalCertDeployer, secretName string, cert *SiteCertificate) error { + if cert == nil { + return nil + } + + secretExists, err := controllerSecretExists(clt, secretName) + if err != nil { + return err + } + caExists, err := controllerCAExists(clt, secretName) + if err != nil { + return err + } + if secretExists && caExists { + return nil + } + + 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 err := clt.CreateSecret(&secretRequest); err != nil && !isControllerConflict(err) { + return err + } + } + + if !caExists { + caRequest := client.CACreateRequest{ + Name: secretName, + Type: "direct", + SecretName: secretName, + } + if err := clt.CreateCA(&caRequest); err != nil && !isControllerConflict(err) { + 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) +} From 475e3e7fba4f8dc46a51c69609ae4877cbfcc536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Wed, 24 Jun 2026 01:06:33 +0300 Subject: [PATCH 37/63] Implement multi-host remote control plane deploy via edgelet. Orchestrate parallel per-host edgelet install, manifest translation, deploy, auth, global certificates, and serial system agent provisioning. --- internal/deploy/airgap/images.go | 4 +- .../controlplane/remote/certificates.go | 46 + .../controlplane/remote/edgelet_host.go | 316 +++++++ .../controlplane/remote/edgelet_host_test.go | 60 ++ .../deploy/controlplane/remote/execute.go | 825 ++---------------- .../deploy/controlplane/remote/manifest.go | 185 ++++ .../controlplane/remote/system_agent.go | 243 ++++++ .../controlplane/remote/system_agent_test.go | 80 ++ .../remote/testdata/edgelet-cp-datasance.yaml | 44 + .../remote/testdata/edgelet-cp-iofog.yaml | 44 + .../deploy/controlplane/remote/translate.go | 400 +++++++++ .../controlplane/remote/translate_test.go | 143 +++ 12 files changed, 1648 insertions(+), 742 deletions(-) create mode 100644 internal/deploy/controlplane/remote/certificates.go create mode 100644 internal/deploy/controlplane/remote/edgelet_host.go create mode 100644 internal/deploy/controlplane/remote/edgelet_host_test.go create mode 100644 internal/deploy/controlplane/remote/manifest.go create mode 100644 internal/deploy/controlplane/remote/system_agent.go create mode 100644 internal/deploy/controlplane/remote/system_agent_test.go create mode 100644 internal/deploy/controlplane/remote/testdata/edgelet-cp-datasance.yaml create mode 100644 internal/deploy/controlplane/remote/testdata/edgelet-cp-iofog.yaml create mode 100644 internal/deploy/controlplane/remote/translate.go create mode 100644 internal/deploy/controlplane/remote/translate_test.go diff --git a/internal/deploy/airgap/images.go b/internal/deploy/airgap/images.go index 3fa5f243e..2466b6eaa 100644 --- a/internal/deploy/airgap/images.go +++ b/internal/deploy/airgap/images.go @@ -258,8 +258,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() } 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 1be038e52..eb3b67c6b 100644 --- a/internal/deploy/controlplane/remote/execute.go +++ b/internal/deploy/controlplane/remote/execute.go @@ -3,71 +3,18 @@ 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.NatsMode == nil { - cfg.NatsMode = iutil.MakeStrPtr(iofog.NatsModeServer) - } else { - // Force server mode for system agents (like router interior) - 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) - } -} - type Options struct { Namespace string Yaml []byte @@ -75,750 +22,148 @@ 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 applyControlPlaneAirgapFlag(namespace string, agent *rsc.RemoteAgent) { - if agent == nil { - return - } - ns, err := config.GetNamespace(namespace) - if err != nil { - return - } - cp, err := ns.GetControlPlane() - if err != nil { - return - } - if remoteCP, ok := cp.(*rsc.RemoteControlPlane); ok { - agent.Airgap = remoteCP.Airgap - } +type hostExecutor struct { + namespace string + name string + cp *rsc.RemoteControlPlane + ctrl *rsc.RemoteController } -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.DeploymentType != nil { - // Use DeploymentType from provided configuration - deploymentType = *systemAgentConfig.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.DeploymentType == nil { - deployAgentConfig.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, - Arch: 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.RouterMode == nil { - interior := iofog.RouterModeInterior - deployAgentConfig.RouterMode = &interior - } else if *deployAgentConfig.RouterMode != iofog.RouterModeInterior { - // Force to interior mode - interior := iofog.RouterModeInterior - deployAgentConfig.RouterMode = &interior - } - - if deployAgentConfig.EdgeRouterPort == nil { - edgeRouterPort := 45671 - deployAgentConfig.EdgeRouterPort = &edgeRouterPort - } - if deployAgentConfig.InterRouterPort == nil { - interRouterPort := 55671 - deployAgentConfig.InterRouterPort = &interRouterPort - } - - if deployAgentConfig.MessagingPort == nil { - messagingPort := 5671 - deployAgentConfig.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 - } - applyControlPlaneAirgapFlag(namespace, &agent) - - // 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() +func (exe hostExecutor) Execute() error { + return deployRemoteControlPlaneHost(exe.namespace, exe.name, exe.cp, exe.ctrl) } -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.DeploymentType != nil { - // Use DeploymentType from provided configuration - deploymentType = *systemAgentConfig.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.DeploymentType == nil { - deployAgentConfig.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, - Arch: 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.RouterMode == nil { - interior := iofog.RouterModeInterior - deployAgentConfig.RouterMode = &interior - } else if *deployAgentConfig.RouterMode != iofog.RouterModeInterior { - // Force to interior mode - interior := iofog.RouterModeInterior - deployAgentConfig.RouterMode = &interior - } - if deployAgentConfig.EdgeRouterPort == nil { - edgeRouterPort := 45671 - deployAgentConfig.EdgeRouterPort = &edgeRouterPort - } - if deployAgentConfig.InterRouterPort == nil { - interRouterPort := 55671 - deployAgentConfig.InterRouterPort = &interRouterPort - } - - if deployAgentConfig.MessagingPort == nil { - messagingPort := 5671 - deployAgentConfig.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 - } - applyControlPlaneAirgapFlag(namespace, &agent) - - // 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: %w", 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: %w", 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 is retired in v3.8 (Keycloak viewer client). -func updateViewerClientRootURL(_ *rsc.RemoteControlPlane, _ string) error { - return nil +func (exe hostExecutor) GetName() string { + return exe.ctrl.Name } -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), - "sudo service iofog-agent restart", - } +func (exe remoteControlPlaneExecutor) Execute() (err error) { + util.SpinStart(fmt.Sprintf("Deploying controlplane %s", exe.GetName())) - // Execute commands - for _, cmd := range cmds { - _, err = ssh.Run(cmd) - if err != nil { - return + if ca := exe.controlPlane.GetTrustCA(); ca != "" { + if err := trust.StoreCA(exe.namespace, ca); err != nil { + return err } } - return -} - -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") - } - - // 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: %w", 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: %w", 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: %w", 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): %w", idx, controller.Name, 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 := trust.WaitForControllerAPI(context.Background(), exe.namespace, endpoint); 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): %w", 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 := 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 } - 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 { + if err := deployControllerRegistry(exe.namespace, exe.controlPlane); err != nil { return err } - // Validate HTTPS configuration - if err := validateMultiControllerHTTPS(controlPlane); err != nil { + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { return err } - - // Validate CA configuration - if err := validateMultiControllerRouterCA(controlPlane); err != nil { + if err := install.DeployGlobalCertificates(clt, globalCertificatesFromCP(exe.controlPlane)); err != nil { return err } - // Validate Vault when set (provider and required provider fields) - if err := validateRemoteVault(controlPlane); err != nil { + if err := deployRemoteSystemAgents(exe.namespace, 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 -} - -// 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) - if err != nil { - return fmt.Errorf("failed to determine deployment type: %w", err) - } - - images, err := deployairgap.CollectControllerImages(exe.ns.Name, remoteControlPlane, isInitial) - 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.NatsAMD64 != "" { - imageList = append(imageList, images.NatsAMD64) - } - if images.NatsARM64 != "" { - imageList = append(imageList, images.NatsARM64) - } - if images.NatsRISCV64 != "" { - imageList = append(imageList, images.NatsRISCV64) - } - if images.NatsARM != "" { - imageList = append(imageList, images.NatsARM) - } - - 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.Arch) - if err != nil { - return fmt.Errorf("controller %s: %w", controller.Name, err) - } - opts, err := deployairgap.ControllerAirgapLoadOptions(controller.SystemAgent.AgentConfiguration) - 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, opts, imageList); err != nil { - return fmt.Errorf("failed to transfer images to controller %s: %w", controller.Name, err) - } - } - - return nil +func (exe remoteControlPlaneExecutor) GetName() string { + return exe.name } func NewExecutor(opt Options) (exe execute.Executor, err error) { - // Check the namespace exists - ns, err := config.GetNamespace(opt.Namespace) + _, err = config.GetNamespace(opt.Namespace) if err != nil { return } - // Read the input file controlPlane, err := rsc.UnmarshallRemoteControlPlane(opt.Yaml) if err != nil { return } - // Validate control plane for multiple controllers - if err := validateMultiControllerConfig(&controlPlane); err != nil { - return nil, err - } + applySystemMicroserviceDefaults(&controlPlane) - // 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 - } - - return newControlPlaneExecutor(controllerExecutors, ns, opt.Name, &controlPlane), nil + return remoteControlPlaneExecutor{ + controlPlane: &controlPlane, + namespace: opt.Namespace, + name: opt.Name, + }, nil } -func runExecutors(executors []execute.Executor) error { - if errs, _ := execute.ForParallel(executors); len(errs) > 0 { - return execute.CoalesceErrors(errs) +func applySystemMicroserviceDefaults(cp *rsc.RemoteControlPlane) { + if cp.SystemMicroservices.Router.AMD64 == "" { + cp.SystemMicroservices.Router.AMD64 = util.GetRouterImage() + } + if cp.SystemMicroservices.Router.ARM64 == "" { + cp.SystemMicroservices.Router.ARM64 = util.GetRouterImage() + } + if cp.SystemMicroservices.Router.RISCV64 == "" { + cp.SystemMicroservices.Router.RISCV64 = util.GetRouterImage() + } + if cp.SystemMicroservices.Router.ARM == "" { + cp.SystemMicroservices.Router.ARM = util.GetRouterImage() + } + if cp.SystemMicroservices.Nats.AMD64 == "" { + cp.SystemMicroservices.Nats.AMD64 = util.GetNatsImage() + } + if cp.SystemMicroservices.Nats.ARM64 == "" { + cp.SystemMicroservices.Nats.ARM64 = util.GetNatsImage() + } + if cp.SystemMicroservices.Nats.RISCV64 == "" { + cp.SystemMicroservices.Nats.RISCV64 = util.GetNatsImage() + } + 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..fecb13155 --- /dev/null +++ b/internal/deploy/controlplane/remote/system_agent.go @@ -0,0 +1,243 @@ +package deployremotecontrolplane + +import ( + "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" + "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() + if _, err := edgelet.Configure(endpoint, user); 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..acfeb2a48 --- /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.1 + arm64: ghcr.io/datasance/nats:2.14.2-rc.1 + riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 + arm: ghcr.io/datasance/nats:2.14.2-rc.1 + 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..7fbe8c096 --- /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.1 + arm64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + riscv64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + arm: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + 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..6f989a173 --- /dev/null +++ b/internal/deploy/controlplane/remote/translate.go @@ -0,0 +1,400 @@ +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). +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). +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..f82b7247d --- /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.1" + + 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.1" +) + +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) +} From 2427ca9349fba5bceb96e29a3d9ff51f692568c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Wed, 24 Jun 2026 01:06:40 +0300 Subject: [PATCH 38/63] Wire standalone RemoteController deploy after control plane provisioning. Reuse remote edgelet orchestration for controller add-ons, validate same-file name and host collisions, and accept RemoteController as a kind alias. --- internal/deploy/controller/remote/remote.go | 202 +++++------------- internal/deploy/execute.go | 6 + internal/deploy/validate/remote_controller.go | 136 ++++++++++++ .../deploy/validate/remote_controller_test.go | 121 +++++++++++ internal/execute/utils.go | 9 +- internal/execute/utils_test.go | 18 ++ 6 files changed, 338 insertions(+), 154 deletions(-) create mode 100644 internal/deploy/validate/remote_controller.go create mode 100644 internal/deploy/validate/remote_controller_test.go create mode 100644 internal/execute/utils_test.go diff --git a/internal/deploy/controller/remote/remote.go b/internal/deploy/controller/remote/remote.go index 8a8ae8212..37a34725a 100644 --- a/internal/deploy/controller/remote/remote.go +++ b/internal/deploy/controller/remote/remote.go @@ -2,9 +2,9 @@ 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" ) @@ -30,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 @@ -54,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 { @@ -83,188 +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() - // v3.8 auth is embedded/external OIDC — configured via ControlPlane spec, not Keycloak env. - // 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 := deployremotecontrolplane.DeployNextSystemAgent(exe.namespace, exe.controlPlane, exe.controller); err != nil { + return err } - // Deploy Controller - if err = deployer.Install(); err != nil { - return + if err := exe.controlPlane.UpdateController(exe.controller); err != nil { + return err } - // Update controller - useHTTPS := exe.controller.Https != nil && exe.controller.Https.Enabled != nil && *exe.controller.Https.Enabled - 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.AMD64 == "" { - exe.controlPlane.SystemMicroservices.Router.AMD64 = util.GetRouterImage() - } - if exe.controlPlane.SystemMicroservices.Router.ARM64 == "" { - exe.controlPlane.SystemMicroservices.Router.ARM64 = util.GetRouterImage() - } - if exe.controlPlane.SystemMicroservices.Router.RISCV64 == "" { - exe.controlPlane.SystemMicroservices.Router.RISCV64 = util.GetRouterImage() - } - if exe.controlPlane.SystemMicroservices.Router.ARM == "" { - } - if exe.controlPlane.SystemMicroservices.Router.ARM == "" { - exe.controlPlane.SystemMicroservices.Router.ARM = util.GetRouterImage() - } - if exe.controlPlane.SystemMicroservices.Nats.AMD64 == "" { - exe.controlPlane.SystemMicroservices.Nats.AMD64 = util.GetNatsImage() +func applySystemMicroserviceDefaults(cp *rsc.RemoteControlPlane) { + if cp.SystemMicroservices.Router.AMD64 == "" { + cp.SystemMicroservices.Router.AMD64 = util.GetRouterImage() } - if exe.controlPlane.SystemMicroservices.Nats.ARM64 == "" { - exe.controlPlane.SystemMicroservices.Nats.ARM64 = util.GetNatsImage() + if cp.SystemMicroservices.Router.ARM64 == "" { + cp.SystemMicroservices.Router.ARM64 = util.GetRouterImage() } - if exe.controlPlane.SystemMicroservices.Nats.RISCV64 == "" { - exe.controlPlane.SystemMicroservices.Nats.RISCV64 = 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/execute.go b/internal/deploy/execute.go index 5bd9295b2..aad6178fb 100644 --- a/internal/deploy/execute.go +++ b/internal/deploy/execute.go @@ -285,6 +285,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) } diff --git a/internal/deploy/validate/remote_controller.go b/internal/deploy/validate/remote_controller.go new file mode 100644 index 000000000..7283cbef6 --- /dev/null +++ b/internal/deploy/validate/remote_controller.go @@ -0,0 +1,136 @@ +package validate + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + + "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 deployDocHeader struct { + Kind config.Kind `yaml:"kind"` + Metadata struct { + Name string `yaml:"name"` + } `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 := os.ReadFile(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/execute/utils.go b/internal/execute/utils.go index 20bff9974..1374eacfb 100644 --- a/internal/execute/utils.go +++ b/internal/execute/utils.go @@ -147,16 +147,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) +} From e7e2ec3f5d5f7f9c18ad3d20796f8452f91dbf0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Wed, 24 Jun 2026 01:06:51 +0300 Subject: [PATCH 39/63] Replace remote controller uninstall with edgelet teardown and add remote func tests. Tear down system agents, deprovision edgelet, and remove control plane workloads on delete, with bats coverage for remote deploy flows. --- internal/delete/controller/remote.go | 41 +---- internal/delete/controller/remote_test.go | 18 +++ internal/delete/controlplane/remote/remote.go | 51 +++++-- .../delete/controlplane/remote/teardown.go | 108 ++++++++++++++ .../controlplane/remote/teardown_test.go | 140 ++++++++++++++++++ test/bats/remote.bats | 79 ++++++++++ test/func/check.bash | 29 ++++ test/func/init.bash | 69 +++++++++ 8 files changed, 486 insertions(+), 49 deletions(-) create mode 100644 internal/delete/controller/remote_test.go create mode 100644 internal/delete/controlplane/remote/teardown.go create mode 100644 internal/delete/controlplane/remote/teardown_test.go create mode 100644 test/bats/remote.bats diff --git a/internal/delete/controller/remote.go b/internal/delete/controller/remote.go index d96e80576..e8b88b44b 100644 --- a/internal/delete/controller/remote.go +++ b/internal/delete/controller/remote.go @@ -1,13 +1,10 @@ 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" ) @@ -30,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/remote/remote.go b/internal/delete/controlplane/remote/remote.go index 2dca2b7c1..736eef4c3 100644 --- a/internal/delete/controlplane/remote/remote.go +++ b/internal/delete/controlplane/remote/remote.go @@ -2,9 +2,9 @@ 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" ) @@ -12,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 application func (exe *Executor) Execute() (err error) { - // Get Control Plane ns, err := config.GetNamespace(exe.namespace) if err != nil { return err @@ -37,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/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/func/check.bash b/test/func/check.bash index 1443040df..9f26d027a 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}') ]] diff --git a/test/func/init.bash b/test/func/init.bash index 229e1bff8..5a5450788 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 From 3a2ead2c791cce50e49a23e654d19b73a3bd347c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 03:15:17 +0300 Subject: [PATCH 40/63] Remove rename and legacy CLI commands. Drop the retired command surface, related config helpers, and generated docs now that v3.8 uses SDK and edgelet workflows exclusively. --- README.md | 2 - docs/md/iofogctl.md | 2 - docs/md/iofogctl_legacy.md | 43 ------ docs/md/iofogctl_rename.md | 33 ----- docs/md/iofogctl_rename_agent.md | 38 ------ docs/md/iofogctl_rename_application.md | 37 ------ docs/md/iofogctl_rename_controller.md | 37 ------ docs/md/iofogctl_rename_edge-resource.md | 37 ------ docs/md/iofogctl_rename_microservice.md | 37 ------ docs/md/iofogctl_rename_namespace.md | 37 ------ internal/cmd/legacy.go | 161 ----------------------- internal/cmd/pkg.go | 9 +- internal/cmd/rename.go | 26 ---- internal/cmd/rename_agent.go | 36 ----- internal/cmd/rename_application.go | 32 ----- internal/cmd/rename_controller.go | 32 ----- internal/cmd/rename_edge_resource.go | 32 ----- internal/cmd/rename_microservice.go | 32 ----- internal/cmd/rename_namespace.go | 30 ----- internal/cmd/root.go | 2 - internal/config/detached_agent.go | 12 -- internal/config/namespace.go | 18 --- internal/detach/agent/execute.go | 8 +- internal/rename/agent/executor.go | 61 --------- internal/rename/application/executor.go | 34 ----- internal/rename/controller/executor.go | 38 ------ internal/rename/edgeresource/executor.go | 52 -------- internal/rename/microservice/executor.go | 61 --------- internal/rename/namespace/executor.go | 24 ---- test/bats/smoke.bats | 4 +- 30 files changed, 7 insertions(+), 1000 deletions(-) delete mode 100644 docs/md/iofogctl_legacy.md delete mode 100644 docs/md/iofogctl_rename.md delete mode 100644 docs/md/iofogctl_rename_agent.md delete mode 100644 docs/md/iofogctl_rename_application.md delete mode 100644 docs/md/iofogctl_rename_controller.md delete mode 100644 docs/md/iofogctl_rename_edge-resource.md delete mode 100644 docs/md/iofogctl_rename_microservice.md delete mode 100644 docs/md/iofogctl_rename_namespace.md delete mode 100644 internal/cmd/legacy.go delete mode 100644 internal/cmd/rename.go delete mode 100644 internal/cmd/rename_agent.go delete mode 100644 internal/cmd/rename_application.go delete mode 100644 internal/cmd/rename_controller.go delete mode 100644 internal/cmd/rename_edge_resource.go delete mode 100644 internal/cmd/rename_microservice.go delete mode 100644 internal/cmd/rename_namespace.go delete mode 100644 internal/rename/agent/executor.go delete mode 100644 internal/rename/application/executor.go delete mode 100644 internal/rename/controller/executor.go delete mode 100644 internal/rename/edgeresource/executor.go delete mode 100644 internal/rename/microservice/executor.go delete mode 100644 internal/rename/namespace/executor.go diff --git a/README.md b/README.md index 9ff8df727..2a30f9e1a 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,10 @@ Available Commands: 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 diff --git a/docs/md/iofogctl.md b/docs/md/iofogctl.md index d675f8b1e..44f4f3762 100644 --- a/docs/md/iofogctl.md +++ b/docs/md/iofogctl.md @@ -29,13 +29,11 @@ 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 rollback](iofogctl_rollback.md) - Rollback ioFog resources * [iofogctl start](iofogctl_start.md) - Starts a resource * [iofogctl stop](iofogctl_stop.md) - Stops a 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_agent.md b/docs/md/iofogctl_rename_agent.md deleted file mode 100644 index da4eeba97..000000000 --- a/docs/md/iofogctl_rename_agent.md +++ /dev/null @@ -1,38 +0,0 @@ -## iofogctl rename agent - -Rename an Agent - -### Synopsis - -Rename an Agent - -``` -iofogctl rename agent NAME NEW_NAME [flags] -``` - -### Examples - -``` -iofogctl rename agent NAME NEW_NAME -``` - -### Options - -``` - --detached Specify command is to run against detached resources - -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 rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed - - diff --git a/docs/md/iofogctl_rename_application.md b/docs/md/iofogctl_rename_application.md deleted file mode 100644 index 88f1c40d3..000000000 --- a/docs/md/iofogctl_rename_application.md +++ /dev/null @@ -1,37 +0,0 @@ -## iofogctl rename application - -Rename an Application - -### Synopsis - -Rename a Application - -``` -iofogctl rename application NAME NEW_NAME [flags] -``` - -### Examples - -``` -iofogctl rename application NAME NEW_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 iofogctl -``` - -### SEE ALSO - -* [iofogctl rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed - - diff --git a/docs/md/iofogctl_rename_controller.md b/docs/md/iofogctl_rename_controller.md deleted file mode 100644 index e262c6b02..000000000 --- a/docs/md/iofogctl_rename_controller.md +++ /dev/null @@ -1,37 +0,0 @@ -## iofogctl rename controller - -Rename a Controller - -### Synopsis - -Rename a Controller - -``` -iofogctl rename controller NAME NEW_NAME [flags] -``` - -### Examples - -``` -iofogctl rename controller NAME NEW_NAME -``` - -### Options - -``` - -h, --help help for controller -``` - -### 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/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/md/iofogctl_rename_microservice.md b/docs/md/iofogctl_rename_microservice.md deleted file mode 100644 index b271fffc6..000000000 --- a/docs/md/iofogctl_rename_microservice.md +++ /dev/null @@ -1,37 +0,0 @@ -## iofogctl rename microservice - -Rename a Microservice - -### Synopsis - -Rename a Microservice - -``` -iofogctl rename microservice NAME NEW_NAME [flags] -``` - -### Examples - -``` -iofogctl rename microservice NAME NEW_NAME -``` - -### 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 iofogctl -``` - -### SEE ALSO - -* [iofogctl rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed - - diff --git a/docs/md/iofogctl_rename_namespace.md b/docs/md/iofogctl_rename_namespace.md deleted file mode 100644 index 97db39fc1..000000000 --- a/docs/md/iofogctl_rename_namespace.md +++ /dev/null @@ -1,37 +0,0 @@ -## iofogctl rename namespace - -Rename a Namespace - -### Synopsis - -Rename a Namespace - -``` -iofogctl rename namespace NAME NEW_NAME [flags] -``` - -### Examples - -``` -iofogctl rename namespace NAME NEW_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 iofogctl -``` - -### SEE ALSO - -* [iofogctl rename](iofogctl_rename.md) - Rename the iofog resources that are currently deployed - - diff --git a/internal/cmd/legacy.go b/internal/cmd/legacy.go deleted file mode 100644 index 51a6d618b..000000000 --- a/internal/cmd/legacy.go +++ /dev/null @@ -1,161 +0,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(install.DefaultLocalContainerEngine, nil) - 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: - out, err := util.Exec("", "edgelet", args[2:]...) - util.Check(err) - fmt.Print(out) - return - case *rsc.RemoteAgent: - 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 edgelet", 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/pkg.go b/internal/cmd/pkg.go index a8ef4ce88..64ddd3991 100644 --- a/internal/cmd/pkg.go +++ b/internal/cmd/pkg.go @@ -2,27 +2,20 @@ 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/rename.go b/internal/cmd/rename.go deleted file mode 100644 index dbc380ff3..000000000 --- a/internal/cmd/rename.go +++ /dev/null @@ -1,26 +0,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 506604a57..000000000 --- a/internal/cmd/rename_agent.go +++ /dev/null @@ -1,36 +0,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 8d9783957..000000000 --- a/internal/cmd/rename_application.go +++ /dev/null @@ -1,32 +0,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 f04f8fd7e..000000000 --- a/internal/cmd/rename_controller.go +++ /dev/null @@ -1,32 +0,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 0ebf8b825..000000000 --- a/internal/cmd/rename_edge_resource.go +++ /dev/null @@ -1,32 +0,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 fff4b822d..000000000 --- a/internal/cmd/rename_microservice.go +++ /dev/null @@ -1,32 +0,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 25f9f5845..000000000 --- a/internal/cmd/rename_namespace.go +++ /dev/null @@ -1,30 +0,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/root.go b/internal/cmd/root.go index d425849c7..07a6ed6c5 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -80,7 +80,6 @@ func NewRootCommand() *cobra.Command { newGetCommand(), newDescribeCommand(), newLogsCommand(), - newLegacyCommand(), newVersionCommand(), newBashCompleteCommand(cmd), newGenerateDocumentationCommand(cmd), @@ -89,7 +88,6 @@ func NewRootCommand() *cobra.Command { newStopCommand(), newMoveCommand(), newRebuildCommand(), - newRenameCommand(), newDockerPruneCommand(), newUpgradeCommand(), newRollbackCommand(), diff --git a/internal/config/detached_agent.go b/internal/config/detached_agent.go index f8f5859a8..87ce6996e 100644 --- a/internal/config/detached_agent.go +++ b/internal/config/detached_agent.go @@ -57,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 73dba9fcc..64b014693 100644 --- a/internal/config/namespace.go +++ b/internal/config/namespace.go @@ -172,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/detach/agent/execute.go b/internal/detach/agent/execute.go index 818fc4a92..8a1315b3d 100644 --- a/internal/detach/agent/execute.go +++ b/internal/detach/agent/execute.go @@ -30,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) diff --git a/internal/rename/agent/executor.go b/internal/rename/agent/executor.go deleted file mode 100644 index c95999d7f..000000000 --- a/internal/rename/agent/executor.go +++ /dev/null @@ -1,61 +0,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 31aa62aed..000000000 --- a/internal/rename/application/executor.go +++ /dev/null @@ -1,34 +0,0 @@ -package application - -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("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 - } - - _, err = clt.GetApplicationByName(name) - if err != nil { - return err - } - - // application.Name = newName - // // _, err = clt.UpdateApplication(&client.ApplicationUpdateRequest{ - // // ID: application.ID, - // // Name: &newName, - // // }) - // fmt.Println(application) - return fmt.Errorf("Application renamed not allowed") -} diff --git a/internal/rename/controller/executor.go b/internal/rename/controller/executor.go deleted file mode 100644 index dd073b505..000000000 --- a/internal/rename/controller/executor.go +++ /dev/null @@ -1,38 +0,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 d6ba62d15..000000000 --- a/internal/rename/edgeresource/executor.go +++ /dev/null @@ -1,52 +0,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 ff71d2de8..000000000 --- a/internal/rename/microservice/executor.go +++ /dev/null @@ -1,61 +0,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: util.GetCliApiVersion(), - 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 2af342199..000000000 --- a/internal/rename/namespace/executor.go +++ /dev/null @@ -1,24 +0,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/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" { From 055bd2ea4323d6c6e0c70f57a599e2424b1df24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 03:15:24 +0300 Subject: [PATCH 41/63] Remove EdgeResource deploy, attach, and get support. Retire the EdgeResource kind and Controller API integration now that application orchestration is handled through the SDK. --- docs/md/iofogctl_attach.md | 1 - docs/md/iofogctl_attach_edge-resource.md | 37 --------- docs/md/iofogctl_delete.md | 1 - docs/md/iofogctl_delete_edge-resource.md | 40 --------- docs/md/iofogctl_deploy.md | 1 - docs/md/iofogctl_describe.md | 1 - docs/md/iofogctl_describe_edge-resource.md | 38 --------- docs/md/iofogctl_detach.md | 1 - docs/md/iofogctl_detach_edge-resource.md | 37 --------- docs/md/iofogctl_get.md | 1 - internal/attach/edgeresource/execute.go | 57 ------------- internal/cmd/attach.go | 1 - internal/cmd/attach_edge_resource.go | 39 --------- internal/cmd/delete.go | 1 - internal/cmd/delete_edge_resource.go | 39 --------- internal/cmd/deploy.go | 1 - internal/cmd/describe.go | 1 - internal/cmd/describe_edge_resource.go | 40 --------- internal/cmd/detach.go | 1 - internal/cmd/detach_edge_resource.go | 37 --------- internal/cmd/get.go | 2 - internal/config/types.go | 1 - internal/delete/edgeresource/factory.go | 44 ---------- internal/deploy/edgeresource/factory.go | 81 ------------------- internal/deploy/execute.go | 8 -- internal/describe/edge_resource.go | 80 ------------------ internal/describe/factory.go | 2 - internal/detach/edgeresource/execute.go | 56 ------------- internal/get/all.go | 14 ---- internal/get/edge_resources.go | 94 ---------------------- internal/get/factory.go | 2 - internal/resource/types.go | 16 ---- internal/util/client/api.go | 12 --- test/func/init.bash | 31 ------- test/func/test.bash | 65 --------------- test/func/vars.bash | 4 - 36 files changed, 887 deletions(-) delete mode 100644 docs/md/iofogctl_attach_edge-resource.md delete mode 100644 docs/md/iofogctl_delete_edge-resource.md delete mode 100644 docs/md/iofogctl_describe_edge-resource.md delete mode 100644 docs/md/iofogctl_detach_edge-resource.md delete mode 100644 internal/attach/edgeresource/execute.go delete mode 100644 internal/cmd/attach_edge_resource.go delete mode 100644 internal/cmd/delete_edge_resource.go delete mode 100644 internal/cmd/describe_edge_resource.go delete mode 100644 internal/cmd/detach_edge_resource.go delete mode 100644 internal/delete/edgeresource/factory.go delete mode 100644 internal/deploy/edgeresource/factory.go delete mode 100644 internal/describe/edge_resource.go delete mode 100644 internal/detach/edgeresource/execute.go delete mode 100644 internal/get/edge_resources.go diff --git a/docs/md/iofogctl_attach.md b/docs/md/iofogctl_attach.md index 8e5d2fd3d..ab893fbc0 100644 --- a/docs/md/iofogctl_attach.md +++ b/docs/md/iofogctl_attach.md @@ -30,7 +30,6 @@ 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 volume-mount](iofogctl_attach_volume-mount.md) - Attach a Volume Mount to existing Agents diff --git a/docs/md/iofogctl_attach_edge-resource.md b/docs/md/iofogctl_attach_edge-resource.md deleted file mode 100644 index f5feacc74..000000000 --- a/docs/md/iofogctl_attach_edge-resource.md +++ /dev/null @@ -1,37 +0,0 @@ -## iofogctl attach edge-resource - -Attach an Edge Resource to an existing Agent - -### Synopsis - -Attach an Edge Resource to an existing Agent. - -``` -iofogctl attach edge-resource NAME VERSION AGENT_NAME [flags] -``` - -### Examples - -``` -iofogctl attach edge-resource NAME VERSION AGENT_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 attach](iofogctl_attach.md) - Attach one ioFog resource to another - - diff --git a/docs/md/iofogctl_delete.md b/docs/md/iofogctl_delete.md index 2dc215d57..24b5cc449 100644 --- a/docs/md/iofogctl_delete.md +++ b/docs/md/iofogctl_delete.md @@ -36,7 +36,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_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_deploy.md b/docs/md/iofogctl_deploy.md index 077c88c10..7405b7699 100644 --- a/docs/md/iofogctl_deploy.md +++ b/docs/md/iofogctl_deploy.md @@ -18,7 +18,6 @@ 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/md/iofogctl_describe.md index a6439b0d7..14e6cbaa6 100644 --- a/docs/md/iofogctl_describe.md +++ b/docs/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_edge-resource.md b/docs/md/iofogctl_describe_edge-resource.md deleted file mode 100644 index a5c4443c2..000000000 --- a/docs/md/iofogctl_describe_edge-resource.md +++ /dev/null @@ -1,38 +0,0 @@ -## iofogctl describe edge-resource - -Get detailed information about an Edge Resource - -### Synopsis - -Get detailed information about an Edge Resource. - -``` -iofogctl describe edge-resource NAME VERSION [flags] -``` - -### Examples - -``` -iofogctl describe edge-resource NAME VERSION -``` - -### Options - -``` - -h, --help help for edge-resource - -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 iofogctl -``` - -### SEE ALSO - -* [iofogctl describe](iofogctl_describe.md) - Get detailed information of an existing resources - - diff --git a/docs/md/iofogctl_detach.md b/docs/md/iofogctl_detach.md index 03b896633..1cae26bf5 100644 --- a/docs/md/iofogctl_detach.md +++ b/docs/md/iofogctl_detach.md @@ -30,7 +30,6 @@ 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 volume-mount](iofogctl_detach_volume-mount.md) - Detach a Volume Mount from existing Agents diff --git a/docs/md/iofogctl_detach_edge-resource.md b/docs/md/iofogctl_detach_edge-resource.md deleted file mode 100644 index 97962d4fb..000000000 --- a/docs/md/iofogctl_detach_edge-resource.md +++ /dev/null @@ -1,37 +0,0 @@ -## iofogctl detach edge-resource - -Detaches an Edge Resource from an Agent - -### Synopsis - -Detaches an Edge Resource from an Agent. - -``` -iofogctl detach edge-resource NAME VERSION AGENT_NAME [flags] -``` - -### Examples - -``` -iofogctl detach edge-resource NAME VERSION AGENT_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 detach](iofogctl_detach.md) - Detach one ioFog resource from another - - diff --git a/docs/md/iofogctl_get.md b/docs/md/iofogctl_get.md index d36e8c25f..cbadbf5b6 100644 --- a/docs/md/iofogctl_get.md +++ b/docs/md/iofogctl_get.md @@ -19,7 +19,6 @@ iofogctl get all namespaces controllers agents - edge-resources application-templates applications system-applications diff --git a/internal/attach/edgeresource/execute.go b/internal/attach/edgeresource/execute.go deleted file mode 100644 index 27ae7a068..000000000 --- a/internal/attach/edgeresource/execute.go +++ /dev/null @@ -1,57 +0,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/cmd/attach.go b/internal/cmd/attach.go index dda76a8a4..5fb3867de 100644 --- a/internal/cmd/attach.go +++ b/internal/cmd/attach.go @@ -15,7 +15,6 @@ func newAttachCommand() *cobra.Command { // Add subcommands cmd.AddCommand( newAttachAgentCommand(), - newAttachEdgeResourceCommand(), newAttachVolumeMountCommand(), newAttachExecCommand(), ) diff --git a/internal/cmd/attach_edge_resource.go b/internal/cmd/attach_edge_resource.go deleted file mode 100644 index bed2aa59f..000000000 --- a/internal/cmd/attach_edge_resource.go +++ /dev/null @@ -1,39 +0,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/delete.go b/internal/cmd/delete.go index a2fe54369..8117ede67 100644 --- a/internal/cmd/delete.go +++ b/internal/cmd/delete.go @@ -49,7 +49,6 @@ func newDeleteCommand() *cobra.Command { newDeleteRegistryCommand(), newDeleteMicroserviceCommand(), newDeleteVolumeCommand(), - newDeleteEdgeResourceCommand(), newDeleteSecretCommand(), newDeleteConfigMapCommand(), newDeleteRoleCommand(), diff --git a/internal/cmd/delete_edge_resource.go b/internal/cmd/delete_edge_resource.go deleted file mode 100644 index b28e784d9..000000000 --- a/internal/cmd/delete_edge_resource.go +++ /dev/null @@ -1,39 +0,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/deploy.go b/internal/cmd/deploy.go index e51d8672d..a0d8f1607 100644 --- a/internal/cmd/deploy.go +++ b/internal/cmd/deploy.go @@ -19,7 +19,6 @@ func newDeployCommand() *cobra.Command { application-template.yaml application.yaml microservice.yaml - edge-resource.yaml catalog.yaml volume.yaml route.yaml diff --git a/internal/cmd/describe.go b/internal/cmd/describe.go index 036d4d68e..78139d44c 100644 --- a/internal/cmd/describe.go +++ b/internal/cmd/describe.go @@ -28,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_edge_resource.go b/internal/cmd/describe_edge_resource.go deleted file mode 100644 index 0ea7abe7b..000000000 --- a/internal/cmd/describe_edge_resource.go +++ /dev/null @@ -1,40 +0,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/detach.go b/internal/cmd/detach.go index 35236e61f..1785765e7 100644 --- a/internal/cmd/detach.go +++ b/internal/cmd/detach.go @@ -15,7 +15,6 @@ func newDetachCommand() *cobra.Command { // Add subcommands cmd.AddCommand( newDetachAgentCommand(), - newDetachEdgeResourceCommand(), newDetachVolumeMountCommand(), newDetachExecCommand(), ) diff --git a/internal/cmd/detach_edge_resource.go b/internal/cmd/detach_edge_resource.go deleted file mode 100644 index fbe66c971..000000000 --- a/internal/cmd/detach_edge_resource.go +++ /dev/null @@ -1,37 +0,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/get.go b/internal/cmd/get.go index b1a8dbb73..331570fc3 100644 --- a/internal/cmd/get.go +++ b/internal/cmd/get.go @@ -15,7 +15,6 @@ func newGetCommand() *cobra.Command { "namespaces", "controllers", "agents", - "edge-resources", "application-templates", "applications", "system-applications", @@ -47,7 +46,6 @@ Resources like Agents will require a working Controller in the namespace to disp namespaces controllers agents - edge-resources application-templates applications system-applications 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/delete/edgeresource/factory.go b/internal/delete/edgeresource/factory.go deleted file mode 100644 index baa700a9e..000000000 --- a/internal/delete/edgeresource/factory.go +++ /dev/null @@ -1,44 +0,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/deploy/edgeresource/factory.go b/internal/deploy/edgeresource/factory.go deleted file mode 100644 index be1afb6bd..000000000 --- a/internal/deploy/edgeresource/factory.go +++ /dev/null @@ -1,81 +0,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 aad6178fb..cb6b1cfcc 100644 --- a/internal/deploy/execute.go +++ b/internal/deploy/execute.go @@ -18,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" @@ -52,7 +51,6 @@ var kindOrder = []config.Kind{ config.NatsUserRuleKind, config.RemoteAgentKind, config.LocalAgentKind, - config.EdgeResourceKind, config.ApplicationTemplateKind, config.VolumeKind, config.OfflineImageKind, @@ -71,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}) } @@ -301,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) @@ -317,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, diff --git a/internal/describe/edge_resource.go b/internal/describe/edge_resource.go deleted file mode 100644 index c63601220..000000000 --- a/internal/describe/edge_resource.go +++ /dev/null @@ -1,80 +0,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 5ea9164de..bd9b9d924 100644 --- a/internal/describe/factory.go +++ b/internal/describe/factory.go @@ -40,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/detach/edgeresource/execute.go b/internal/detach/edgeresource/execute.go deleted file mode 100644 index fe98e445f..000000000 --- a/internal/detach/edgeresource/execute.go +++ /dev/null @@ -1,56 +0,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/get/all.go b/internal/get/all.go index b12a7c6a1..2749d6c25 100644 --- a/internal/get/all.go +++ b/internal/get/all.go @@ -2,7 +2,6 @@ package get import ( "github.com/eclipse-iofog/iofogctl/internal/config" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" ) type tableFunc = func(string, tableChannel) @@ -46,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 { @@ -123,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/edge_resources.go b/internal/get/edge_resources.go deleted file mode 100644 index ea65fcf16..000000000 --- a/internal/get/edge_resources.go +++ /dev/null @@ -1,94 +0,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 3a3cadb12..ac8625d7e 100644 --- a/internal/get/factory.go +++ b/internal/get/factory.go @@ -31,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/resource/types.go b/internal/resource/types.go index 2e555dab0..b15aa5356 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -219,22 +219,6 @@ type AgentStatus struct { ControlPlaneQuiesced bool `json:"controlPlaneQuiesced" yaml:"controlPlaneQuiesced"` } -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"` -} - -type EdgeResourceHTTPInterface = client.HTTPEdgeResource - -type Display = client.EdgeResourceDisplay -type HTTPEndpoint = client.HTTPEndpoint - // ArchStringToID maps canonical architecture names to Controller archId values. func ArchStringToID(name string) (int64, bool) { id, ok := arch.NameToID[name] diff --git a/internal/util/client/api.go b/internal/util/client/api.go index b04f61000..85c487ae4 100644 --- a/internal/util/client/api.go +++ b/internal/util/client/api.go @@ -46,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 { diff --git a/test/func/init.bash b/test/func/init.bash index 5a5450788..d3b9f284f 100755 --- a/test/func/init.bash +++ b/test/func/init.bash @@ -542,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" From 4a1c6e5f31f5a311595bb4726bcdf9abb55b6274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 03:15:30 +0300 Subject: [PATCH 42/63] Retire legacy controller SSH and container install assets. Remove embedded controller install scripts and container deploy helpers, stub LocalController add-on deploy, and read controller logs from edgelet instead of iofog-controller. --- assets/airgap-controller/check_prereqs.sh | 9 - assets/airgap-controller/init.sh | 296 ------ .../install_container_engine.sh | 184 ---- assets/airgap-controller/install_iofog.sh | 222 ----- assets/airgap-controller/set_env.sh | 20 - assets/airgap-controller/uninstall_iofog.sh | 106 --- assets/container-controller/check_prereqs.sh | 9 - assets/container-controller/init.sh | 296 ------ .../install_container_engine.sh | 302 ------ assets/container-controller/install_iofog.sh | 232 ----- assets/container-controller/set_env.sh | 20 - .../container-controller/uninstall_iofog.sh | 106 --- assets/controller/check_prereqs.sh | 9 - assets/controller/install_iofog.sh | 146 --- assets/controller/install_node.sh | 36 - .../service/iofog-controller.initctl | 11 - .../service/iofog-controller.systemd | 11 - .../service/iofog-controller.update-rc | 18 - assets/controller/set_env.sh | 26 - assets/controller/uninstall_iofog.sh | 33 - assets/edgelet/scripts/install.sh | 6 +- assets/embed.go | 4 +- internal/delete/controller/local.go | 33 +- internal/delete/controlplane/local/local.go | 6 - internal/deploy/controller/local/local.go | 138 +-- internal/logs/local_controller.go | 4 +- internal/logs/remote_controller.go | 2 +- pkg/containerengine/docker/client.go | 2 +- pkg/iofog/install/controller.go | 894 ------------------ pkg/iofog/install/local_container.go | 119 --- pkg/iofog/install/pkg.go | 14 - pkg/iofog/install/types.go | 17 + 32 files changed, 39 insertions(+), 3292 deletions(-) delete mode 100755 assets/airgap-controller/check_prereqs.sh delete mode 100755 assets/airgap-controller/init.sh delete mode 100644 assets/airgap-controller/install_container_engine.sh delete mode 100644 assets/airgap-controller/install_iofog.sh delete mode 100755 assets/airgap-controller/set_env.sh delete mode 100644 assets/airgap-controller/uninstall_iofog.sh delete mode 100755 assets/container-controller/check_prereqs.sh delete mode 100755 assets/container-controller/init.sh delete mode 100755 assets/container-controller/install_container_engine.sh delete mode 100755 assets/container-controller/install_iofog.sh delete mode 100755 assets/container-controller/set_env.sh delete mode 100644 assets/container-controller/uninstall_iofog.sh delete mode 100755 assets/controller/check_prereqs.sh delete mode 100755 assets/controller/install_iofog.sh delete mode 100755 assets/controller/install_node.sh delete mode 100644 assets/controller/service/iofog-controller.initctl delete mode 100644 assets/controller/service/iofog-controller.systemd delete mode 100644 assets/controller/service/iofog-controller.update-rc delete mode 100755 assets/controller/set_env.sh delete mode 100644 assets/controller/uninstall_iofog.sh 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-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/scripts/install.sh b/assets/edgelet/scripts/install.sh index 5bbe8433e..38e2858f5 100644 --- a/assets/edgelet/scripts/install.sh +++ b/assets/edgelet/scripts/install.sh @@ -2,9 +2,9 @@ # install.sh — Edgelet installer (potctl chunked fork; upstream parity for upgrade/rollback) # # Usage: -# sudo ./install.sh --version=v1.0.0-rc.3 -# sudo ./install.sh --airgap --bin-path=/path/to/edgelet-linux-amd64 --version=v1.0.0-rc.3 -# sudo ./install.sh --upgrade --version=v1.0.0-rc.3 +# sudo ./install.sh --version=v1.0.0-rc.4 +# sudo ./install.sh --airgap --bin-path=/path/to/edgelet-linux-amd64 --version=v1.0.0-rc.4 +# sudo ./install.sh --upgrade --version=v1.0.0-rc.4 # sudo ./install.sh --rollback # # potctl deploy uses --skip-config and --skip-start (config/start handled by iofogctl/potctl). diff --git a/assets/embed.go b/assets/embed.go index a1bf1ea7d..965aa9631 100644 --- a/assets/embed.go +++ b/assets/embed.go @@ -2,7 +2,7 @@ package assets import "embed" -// FS holds install scripts and service unit templates bundled with the CLI. +// FS holds install scripts bundled with the CLI. // -//go:embed controller container-controller airgap-controller edgelet +//go:embed edgelet var FS embed.FS diff --git a/internal/delete/controller/local.go b/internal/delete/controller/local.go index 361247dd2..d1cb1251f 100644 --- a/internal/delete/controller/local.go +++ b/internal/delete/controller/local.go @@ -1,29 +1,22 @@ 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 { @@ -31,10 +24,6 @@ func (exe *LocalExecutor) GetName() string { } func (exe *LocalExecutor) Execute() error { - if err := exe.deleteLegacyControllerContainer(); err != nil { - util.PrintNotify(fmt.Sprintf("Could not clean Controller container: %v", err)) - } - ns, err := config.GetNamespace(exe.namespace) if err != nil { return err @@ -45,11 +34,3 @@ func (exe *LocalExecutor) Execute() error { ns.SetControlPlane(exe.controlPlane) return config.Flush() } - -func (exe *LocalExecutor) deleteLegacyControllerContainer() error { - client, err := install.NewLocalContainerClient(install.DefaultLocalContainerEngine, nil) - if err != nil { - return err - } - return client.CleanContainer(exe.localControllerConfig.ContainerName) -} diff --git a/internal/delete/controlplane/local/local.go b/internal/delete/controlplane/local/local.go index 9f2718f67..14ba94e54 100644 --- a/internal/delete/controlplane/local/local.go +++ b/internal/delete/controlplane/local/local.go @@ -2,7 +2,6 @@ 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" @@ -45,11 +44,6 @@ func (exe *Executor) Execute() (err error) { if err := teardownEdgeletControlPlane(exe.namespace, controlPlane, name); err != nil { return err } - } else { - executor := deletecontroller.NewLocalExecutor(controlPlane, exe.namespace, name) - if err := executor.Execute(); err != nil { - return err - } } ns.DeleteControlPlane() diff --git a/internal/deploy/controller/local/local.go b/internal/deploy/controller/local/local.go index 1a80428a6..47969aac1 100644 --- a/internal/deploy/controller/local/local.go +++ b/internal/deploy/controller/local/local.go @@ -1,25 +1,16 @@ 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 { @@ -38,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 { @@ -59,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 } @@ -74,85 +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(install.DefaultLocalContainerEngine, nil) - 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(rsc.AuthToCPV3(controlPlane.Auth)), 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 { @@ -160,43 +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 install.RemoteSystemMicroservices, nats *rsc.NatsEnabledConfig) *install.LocalSystemImages { - out := &install.LocalSystemImages{ - Router: firstSystemImage(s.Router), - Nats: firstSystemImage(s.Nats), - } - if nats != nil && nats.Enabled != nil { - out.NatsEnabled = nats.Enabled - } - return out -} - -func firstSystemImage(images install.RemoteSystemImages) string { - for _, image := range []string{images.AMD64, images.ARM64, images.RISCV64, images.ARM} { - if image != "" { - return image - } - } - return "" + return util.NewInputError("LocalController add-on deploy via edgelet is not implemented yet") } func Validate(ctrl rsc.Controller) error { diff --git a/internal/logs/local_controller.go b/internal/logs/local_controller.go index ea0251e76..4757c1cca 100644 --- a/internal/logs/local_controller.go +++ b/internal/logs/local_controller.go @@ -28,13 +28,11 @@ func (exe *localControllerExecutor) Execute() error { 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/remote_controller.go b/internal/logs/remote_controller.go index 292315061..07507bdfa 100644 --- a/internal/logs/remote_controller.go +++ b/internal/logs/remote_controller.go @@ -60,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/pkg/containerengine/docker/client.go b/pkg/containerengine/docker/client.go index 75579f532..e50bdde7b 100644 --- a/pkg/containerengine/docker/client.go +++ b/pkg/containerengine/docker/client.go @@ -1,4 +1,4 @@ -// Package docker wraps the Moby API for legacy local control plane container operations. +// 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 diff --git a/pkg/iofog/install/controller.go b/pkg/iofog/install/controller.go index 9ac118e7b..0e74d1ddf 100644 --- a/pkg/iofog/install/controller.go +++ b/pkg/iofog/install/controller.go @@ -2,904 +2,10 @@ package install import ( "errors" - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - "time" "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"` - 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 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 compatibility - reader := strings.NewReader(staticFile) - if err := ctrl.ssh.CopyTo(reader, destDir, filename, "0775", int64(len(staticFile))); err != nil { - return err - } - - return nil -} - -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 (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() - } - - // 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, - } - for _, script := range scripts { - if err := ctrl.CopyScript(assetPrefix, script, ctrl.ctrlDir); 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.AMD64 != "" { - env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_1=%s\"", ctrl.SystemMicroservices.Router.AMD64)) - } - if ctrl.SystemMicroservices.Router.ARM64 != "" { - env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_2=%s\"", ctrl.SystemMicroservices.Router.ARM64)) - } - if ctrl.SystemMicroservices.Router.RISCV64 != "" { - env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_3=%s\"", ctrl.SystemMicroservices.Router.RISCV64)) - } - if ctrl.SystemMicroservices.Router.ARM != "" { - env = append(env, fmt.Sprintf("\"ROUTER_IMAGE_4=%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 -} - -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 appendNatsEnv(env []string, ctrl *Controller) []string { - natsAMD64 := ctrl.SystemMicroservices.Nats.AMD64 - natsARM64 := ctrl.SystemMicroservices.Nats.ARM64 - natsRISCV64 := ctrl.SystemMicroservices.Nats.RISCV64 - natsARM := ctrl.SystemMicroservices.Nats.ARM - if natsAMD64 == "" && natsARM64 != "" { - natsAMD64 = natsARM64 - } - if natsARM64 == "" && natsAMD64 != "" { - natsARM64 = natsAMD64 - } - if natsRISCV64 == "" && natsARM != "" { - natsRISCV64 = natsARM - } - if natsARM == "" && natsRISCV64 != "" { - natsARM = natsRISCV64 - } - if natsAMD64 == "" && natsARM64 == "" && natsRISCV64 == "" { - natsAMD64 = util.GetNatsImage() - } - if natsAMD64 != "" { - env = append(env, fmt.Sprintf("\"NATS_IMAGE_1=%s\"", natsAMD64)) - } - if natsARM64 != "" { - env = append(env, fmt.Sprintf("\"NATS_IMAGE_2=%s\"", natsARM64)) - } - if natsRISCV64 != "" { - env = append(env, fmt.Sprintf("\"NATS_IMAGE_3=%s\"", natsRISCV64)) - } - if natsARM != "" { - env = append(env, fmt.Sprintf("\"NATS_IMAGE_4=%s\"", natsARM)) - } - natsEnabled := true - if ctrl.NatsEnabled != nil { - natsEnabled = *ctrl.NatsEnabled - } - env = append(env, fmt.Sprintf("\"NATS_ENABLED=%t\"", natsEnabled)) - return env -} - -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)) - } - if ctrl.Vault.Provider != "" { - env = append(env, fmt.Sprintf("\"VAULT_PROVIDER=%s\"", ctrl.Vault.Provider)) - } - 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 -} - -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)) - } - if h.Token != "" { - env = append(env, fmt.Sprintf("\"VAULT_HASHICORP_TOKEN=%s\"", h.Token)) - } - 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 -} - -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)) - } - 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)) - } - if g.Credentials != "" { - env = append(env, fmt.Sprintf("\"VAULT_GOOGLE_CREDENTIALS=%s\"", g.Credentials)) - } - return env -} - -func appendLogLevelEnv(env []string, ctrl *Controller) []string { - if ctrl.LogLevel != "" { - env = append(env, fmt.Sprintf("\"LOG_LEVEL=%s\"", ctrl.LogLevel)) - } - 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 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 { - 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) 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 - } - - 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{ - "ca.crt": TLSCert, - "tls.crt": TLSCert, - "tls.key": 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 -} - // GlobalCertificates holds optional router/NATS CA blocks deployed once via Controller API. type GlobalCertificates struct { RouterSiteCA *SiteCertificate diff --git a/pkg/iofog/install/local_container.go b/pkg/iofog/install/local_container.go index 5359bee41..7f64df532 100644 --- a/pkg/iofog/install/local_container.go +++ b/pkg/iofog/install/local_container.go @@ -1,15 +1,11 @@ package install import ( - "fmt" "regexp" "time" "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" - "github.com/eclipse-iofog/iofogctl/pkg/iofog" - "github.com/eclipse-iofog/iofogctl/pkg/util" ) // LocalContainer struct to encapsulate utilities around docker @@ -47,121 +43,16 @@ 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 } -func GetLocalContainerName(kind string, _ bool) string { - if kind != "controller" { - return "" - } - return sanitizeContainerName("iofog-controller") -} - func sanitizeContainerName(name string) string { r := regexp.MustCompile("[^a-zA-Z0-9_.-]") return r.ReplaceAllString(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 generates a static controller config -func NewLocalControllerConfig(image string, credentials Credentials, auth Auth, db Database, events Events, systemImages *LocalSystemImages) *LocalContainerConfig { - if image == "" { - image = util.GetControllerImage() - } - - sslValue := "false" - if db.SSL != nil { - sslValue = fmt.Sprintf("%t", *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=" + fmt.Sprintf("%d", db.Port), - "DB_NAME=" + db.DatabaseName, - "DB_USE_SSL=" + sslValue, - "DB_SSL_CA=" + caValue, - } - _ = auth - - if events.AuditEnabled != nil { - envs = append(envs, fmt.Sprintf("EVENT_AUDIT_ENABLED=%t", *events.AuditEnabled)) - 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)) - } - 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 dials the container engine using ResolveContainerEngineURL. func NewLocalContainerClient(engine string, agentCfg *client.AgentConfiguration) (*LocalContainer, error) { cli, err := NewContainerEngineClient(engine, agentCfg) @@ -228,16 +119,6 @@ func (lc *LocalContainer) DeployContainer(containerConfig *LocalContainerConfig) return lc.client.DeployContainer(opts, pullOpts) } -// 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 - } - controllerEndpoint = fmt.Sprintf("http://%s:%s", host, iofog.ControllerPortString) - return -} - func (lc *LocalContainer) GetContainerIP(name string) (ip string, err error) { return lc.client.GetContainerIP(name) } diff --git a/pkg/iofog/install/pkg.go b/pkg/iofog/install/pkg.go index 58b94d58e..d34a38cf5 100644 --- a/pkg/iofog/install/pkg.go +++ b/pkg/iofog/install/pkg.go @@ -14,14 +14,7 @@ var pkg struct { edgeletScriptBundled string edgeletScriptUninstall string edgeletLibScripts []string - controllerScriptPrereq string - controllerScriptInit string - controllerScriptInstallContainerEngine string - controllerScriptSetEnv string - controllerScriptInstall string - controllerScriptUninstall string iofogDir string - controllerDir string } func init() { @@ -46,12 +39,5 @@ func init() { "lib/container_engine.sh", "lib/container_mounts.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.iofogDir = "/etc/iofog" - pkg.controllerDir = "/etc/iofog/controller" } diff --git a/pkg/iofog/install/types.go b/pkg/iofog/install/types.go index 5c2df0eb1..b8c37998e 100644 --- a/pkg/iofog/install/types.go +++ b/pkg/iofog/install/types.go @@ -68,6 +68,23 @@ 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 From a637721d587256e2fe9afcb907f25c7299f64b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 03:15:36 +0300 Subject: [PATCH 43/63] Bump iofog-go-sdk to v3.8.0-rc.6 and refresh component tags. Drop the Datasance SDK replace directive and align golden fixtures and unit tests with controller 3.8.0-rc.4, NATS 2.14.2-rc.2, and edgelet v1.0.0-rc.4. --- go.mod | 4 +--- go.sum | 4 ++-- internal/deploy/airgap/binary_test.go | 4 ++-- .../k8s/testdata/cp-cr-datasance.yaml | 4 ++-- .../k8s/testdata/cp-cr-iofog.yaml | 4 ++-- .../deploy/controlplane/k8s/translate_test.go | 22 +++++++++---------- .../local/testdata/edgelet-cp-datasance.yaml | 8 +++---- .../local/testdata/edgelet-cp-iofog.yaml | 8 +++---- .../controlplane/local/translate_test.go | 6 ++--- .../remote/testdata/edgelet-cp-datasance.yaml | 8 +++---- .../remote/testdata/edgelet-cp-iofog.yaml | 8 +++---- .../controlplane/remote/translate_test.go | 4 ++-- .../testdata/k8s/controlplane-datasance.yaml | 4 ++-- .../testdata/k8s/controlplane-iofog.yaml | 4 ++-- .../resource/testdata/k8s/controlplane.yaml | 4 ++-- .../local/controlplane-datasance.yaml | 8 +++---- .../local/controlplane-private-registry.yaml | 8 +++---- .../resource/testdata/local/controlplane.yaml | 10 ++++----- .../remote/controlplane-datasance.yaml | 8 +++---- .../testdata/remote/controlplane-iofog.yaml | 8 +++---- .../util/client/client_local_cp_sync_test.go | 2 +- internal/util/client/client_sync_test.go | 4 ++-- pkg/iofog/install/edgelet_procedures_test.go | 6 ++--- pkg/iofog/install/edgelet_remote_test.go | 2 +- pkg/iofog/install/procedures_test.go | 4 ++-- pkg/util/edgelet_binary_test.go | 8 +++---- versions.mk | 6 ++--- 27 files changed, 84 insertions(+), 86 deletions(-) diff --git a/go.mod b/go.mod index d33e34ee3..08110bdee 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.4 require ( github.com/briandowns/spinner v1.23.1 - github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.4 + github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.6 github.com/eclipse-iofog/iofog-operator/v3 v3.8.0-rc.1 github.com/gorilla/websocket v1.5.3 github.com/mitchellh/go-homedir v1.1.0 @@ -141,8 +141,6 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect ) -replace github.com/eclipse-iofog/iofog-go-sdk/v3 => github.com/Datasance/iofog-go-sdk/v3 v3.8.0-rc.4 - exclude github.com/Sirupsen/logrus v1.4.2 exclude github.com/Sirupsen/logrus v1.4.1 diff --git a/go.sum b/go.sum index cd26a0881..d85757cd4 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ 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/Datasance/iofog-go-sdk/v3 v3.8.0-rc.4 h1:2RdokTAstIB/290zu4F1ubchi/o6+yzgv7WQkAzeCoo= -github.com/Datasance/iofog-go-sdk/v3 v3.8.0-rc.4/go.mod h1:MU+YPxFRGFzBMboi1khtEGAoRwaF4yJX3KuBwFcfCKg= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= @@ -51,6 +49,8 @@ github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf 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.8.0-rc.6 h1:3S2t7XGYxUtYCUnGDueJM1oJghXgIn5xOs58db4xQ8Y= +github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.6/go.mod h1:MU+YPxFRGFzBMboi1khtEGAoRwaF4yJX3KuBwFcfCKg= 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= diff --git a/internal/deploy/airgap/binary_test.go b/internal/deploy/airgap/binary_test.go index 88778b19e..aed9e4115 100644 --- a/internal/deploy/airgap/binary_test.go +++ b/internal/deploy/airgap/binary_test.go @@ -15,7 +15,7 @@ import ( 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.3/edgelet-linux-amd64" { + if r.URL.Path != "/v1.0.0-rc.4/edgelet-linux-amd64" { http.NotFound(w, r) return } @@ -24,7 +24,7 @@ func TestEnsureEdgeletBinaryUsesCache(t *testing.T) { defer server.Close() util.SetEdgeletReleaseBaseForTest(server.URL) - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.3") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.4") t.Cleanup(func() { util.ResetEdgeletReleaseBaseForTest() util.ResetEdgeletBinaryVersionForTest() diff --git a/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml b/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml index 3205cad42..d342d21e7 100644 --- a/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml +++ b/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml @@ -25,9 +25,9 @@ spec: cleanupInterval: 86400 captureIpAddress: true images: - controller: ghcr.io/datasance/controller:3.8.0-rc.2 + controller: ghcr.io/datasance/controller:3.8.0-rc.4 router: ghcr.io/datasance/router:3.8.0-rc.1 - nats: ghcr.io/datasance/nats:2.14.2-rc.1 + nats: ghcr.io/datasance/nats:2.14.2-rc.2 nats: enabled: true jetStream: diff --git a/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml b/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml index 5a9f10359..50f01819e 100644 --- a/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml +++ b/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml @@ -25,9 +25,9 @@ spec: cleanupInterval: 86400 captureIpAddress: true images: - controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.2 + controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.4 router: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 - nats: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + nats: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 nats: enabled: true jetStream: diff --git a/internal/deploy/controlplane/k8s/translate_test.go b/internal/deploy/controlplane/k8s/translate_test.go index 3f4f97264..acc28e319 100644 --- a/internal/deploy/controlplane/k8s/translate_test.go +++ b/internal/deploy/controlplane/k8s/translate_test.go @@ -64,9 +64,9 @@ func TestTranslateToControlPlaneCR_DatasanceGolden(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "datasance.com/v3", crName: "pot", - controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.2", + controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.4", routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", - natsImage: "ghcr.io/datasance/nats:2.14.2-rc.1", + natsImage: "ghcr.io/datasance/nats:2.14.2-rc.2", }) want := loadExpectedCR(t, "cp-cr-datasance.yaml") assertTranslatedCR(t, got, want) @@ -77,9 +77,9 @@ func TestTranslateToControlPlaneCR_IofogGolden(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "iofog.org/v3", crName: "iofog", - controllerImage: "ghcr.io/eclipse-iofog/controller:3.8.0-rc.2", + controllerImage: "ghcr.io/eclipse-iofog/controller:3.8.0-rc.4", routerImage: "ghcr.io/eclipse-iofog/router:3.8.0-rc.1", - natsImage: "ghcr.io/eclipse-iofog/nats:2.14.2-rc.1", + natsImage: "ghcr.io/eclipse-iofog/nats:2.14.2-rc.2", }) want := loadExpectedCR(t, "cp-cr-iofog.yaml") assertTranslatedCR(t, got, want) @@ -91,12 +91,12 @@ func TestTranslateToControlPlaneCR_StripsOperatorImage(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "datasance.com/v3", crName: "pot", - controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.2", + controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.4", routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", - natsImage: "ghcr.io/datasance/nats:2.14.2-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.2", got.Spec.Images.Controller) + require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.4", got.Spec.Images.Controller) } func TestTranslateToControlPlaneCR_DefaultImagesWhenOmitted(t *testing.T) { @@ -107,13 +107,13 @@ func TestTranslateToControlPlaneCR_DefaultImagesWhenOmitted(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "datasance.com/v3", crName: "pot", - controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.2", + controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.4", routerImage: "ghcr.io/datasance/router:3.8.0-rc.1", - natsImage: "ghcr.io/datasance/nats:2.14.2-rc.1", + natsImage: "ghcr.io/datasance/nats:2.14.2-rc.2", }) - require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.2", got.Spec.Images.Controller) + require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.4", 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.1", got.Spec.Images.Nats) + require.Equal(t, "ghcr.io/datasance/nats:2.14.2-rc.2", got.Spec.Images.Nats) } func TestTranslateToControlPlaneCR_CRNameFromLdflagDefault(t *testing.T) { diff --git a/internal/deploy/controlplane/local/testdata/edgelet-cp-datasance.yaml b/internal/deploy/controlplane/local/testdata/edgelet-cp-datasance.yaml index 14f2349de..5ad54a962 100644 --- a/internal/deploy/controlplane/local/testdata/edgelet-cp-datasance.yaml +++ b/internal/deploy/controlplane/local/testdata/edgelet-cp-datasance.yaml @@ -28,10 +28,10 @@ spec: 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.1 - arm64: ghcr.io/datasance/nats:2.14.2-rc.1 - riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 - arm: ghcr.io/datasance/nats:2.14.2-rc.1 + 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 index 137d9573a..a0986b0d1 100644 --- a/internal/deploy/controlplane/local/testdata/edgelet-cp-iofog.yaml +++ b/internal/deploy/controlplane/local/testdata/edgelet-cp-iofog.yaml @@ -26,10 +26,10 @@ spec: 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.1 - arm64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 - riscv64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 - arm: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + 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/translate_test.go b/internal/deploy/controlplane/local/translate_test.go index a20efbb98..7318dba6f 100644 --- a/internal/deploy/controlplane/local/translate_test.go +++ b/internal/deploy/controlplane/local/translate_test.go @@ -14,13 +14,13 @@ import ( const testNamespace = "test-ns" const ( - testControllerImage = "ghcr.io/datasance/controller:3.8.0-rc.2" + testControllerImage = "ghcr.io/datasance/controller:3.8.0-rc.4" testRouterImage = "ghcr.io/datasance/router:3.8.0-rc.1" - testNatsImage = "ghcr.io/datasance/nats:2.14.2-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.1" + testIofogNatsImage = "ghcr.io/eclipse-iofog/nats:2.14.2-rc.2" ) func resourceFixturePath(name string) string { diff --git a/internal/deploy/controlplane/remote/testdata/edgelet-cp-datasance.yaml b/internal/deploy/controlplane/remote/testdata/edgelet-cp-datasance.yaml index acfeb2a48..f67c4aa27 100644 --- a/internal/deploy/controlplane/remote/testdata/edgelet-cp-datasance.yaml +++ b/internal/deploy/controlplane/remote/testdata/edgelet-cp-datasance.yaml @@ -35,10 +35,10 @@ spec: 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.1 - arm64: ghcr.io/datasance/nats:2.14.2-rc.1 - riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 - arm: ghcr.io/datasance/nats:2.14.2-rc.1 + 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 index 7fbe8c096..98608c10d 100644 --- a/internal/deploy/controlplane/remote/testdata/edgelet-cp-iofog.yaml +++ b/internal/deploy/controlplane/remote/testdata/edgelet-cp-iofog.yaml @@ -35,10 +35,10 @@ spec: 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.1 - arm64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 - riscv64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 - arm: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + 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_test.go b/internal/deploy/controlplane/remote/translate_test.go index f82b7247d..a8312ca04 100644 --- a/internal/deploy/controlplane/remote/translate_test.go +++ b/internal/deploy/controlplane/remote/translate_test.go @@ -16,11 +16,11 @@ 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.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.1" + testIofogNatsImage = "ghcr.io/eclipse-iofog/nats:2.14.2-rc.2" ) func resourceFixturePath(name string) string { diff --git a/internal/resource/testdata/k8s/controlplane-datasance.yaml b/internal/resource/testdata/k8s/controlplane-datasance.yaml index e2a2ead7b..1c88b1929 100644 --- a/internal/resource/testdata/k8s/controlplane-datasance.yaml +++ b/internal/resource/testdata/k8s/controlplane-datasance.yaml @@ -26,9 +26,9 @@ events: captureIpAddress: true images: operator: ghcr.io/datasance/operator:3.8.0-rc.1 - controller: ghcr.io/datasance/controller:3.8.0-rc.2 + controller: ghcr.io/datasance/controller:3.8.0-rc.4 router: ghcr.io/datasance/router:3.8.0-rc.1 - nats: ghcr.io/datasance/nats:2.14.2-rc.1 + nats: ghcr.io/datasance/nats:2.14.2-rc.2 nats: enabled: true jetStream: diff --git a/internal/resource/testdata/k8s/controlplane-iofog.yaml b/internal/resource/testdata/k8s/controlplane-iofog.yaml index 5ddd6d016..e69d78526 100644 --- a/internal/resource/testdata/k8s/controlplane-iofog.yaml +++ b/internal/resource/testdata/k8s/controlplane-iofog.yaml @@ -26,9 +26,9 @@ events: captureIpAddress: true images: operator: ghcr.io/eclipse-iofog/operator:3.8.0-rc.1 - controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.2 + controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.4 router: ghcr.io/eclipse-iofog/router:3.8.0-rc.1 - nats: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + nats: ghcr.io/eclipse-iofog/nats:2.14.2-rc.2 nats: enabled: true jetStream: diff --git a/internal/resource/testdata/k8s/controlplane.yaml b/internal/resource/testdata/k8s/controlplane.yaml index bae157d14..8596d22cc 100644 --- a/internal/resource/testdata/k8s/controlplane.yaml +++ b/internal/resource/testdata/k8s/controlplane.yaml @@ -53,9 +53,9 @@ spec: # id: controller # secret: "" images: - controller: ghcr.io/datasance/controller:3.8.0-rc.2 + controller: ghcr.io/datasance/controller:3.8.0-rc.4 router: ghcr.io/datasance/router:3.8.0-rc.1 - nats: ghcr.io/datasance/nats:2.14.2-rc.1 + nats: ghcr.io/datasance/nats:2.14.2-rc.2 nats: enabled: true jetStream: diff --git a/internal/resource/testdata/local/controlplane-datasance.yaml b/internal/resource/testdata/local/controlplane-datasance.yaml index 476fb128f..3211aec6d 100644 --- a/internal/resource/testdata/local/controlplane-datasance.yaml +++ b/internal/resource/testdata/local/controlplane-datasance.yaml @@ -23,10 +23,10 @@ systemMicroservices: 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.1 - arm64: ghcr.io/datasance/nats:2.14.2-rc.1 - riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 - arm: ghcr.io/datasance/nats:2.14.2-rc.1 + 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: diff --git a/internal/resource/testdata/local/controlplane-private-registry.yaml b/internal/resource/testdata/local/controlplane-private-registry.yaml index d5b596f68..e01f869de 100644 --- a/internal/resource/testdata/local/controlplane-private-registry.yaml +++ b/internal/resource/testdata/local/controlplane-private-registry.yaml @@ -27,10 +27,10 @@ systemMicroservices: 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.1 - arm64: ghcr.io/datasance/nats:2.14.2-rc.1 - riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 - arm: ghcr.io/datasance/nats:2.14.2-rc.1 + 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: diff --git a/internal/resource/testdata/local/controlplane.yaml b/internal/resource/testdata/local/controlplane.yaml index 120eb424f..5a410194a 100644 --- a/internal/resource/testdata/local/controlplane.yaml +++ b/internal/resource/testdata/local/controlplane.yaml @@ -13,7 +13,7 @@ spec: consoleUrl: http://192.168.1.6 logLevel: info package: - image: ghcr.io/datasance/controller:3.8.0-rc.2 + image: ghcr.io/datasance/controller:3.8.0-rc.4 auth: mode: embedded insecureAllowHttp: true @@ -28,10 +28,10 @@ spec: 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.1 - arm64: ghcr.io/datasance/nats:2.14.2-rc.1 - riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 - arm: ghcr.io/datasance/nats:2.14.2-rc.1 + 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: diff --git a/internal/resource/testdata/remote/controlplane-datasance.yaml b/internal/resource/testdata/remote/controlplane-datasance.yaml index 602da599b..b38f83e33 100644 --- a/internal/resource/testdata/remote/controlplane-datasance.yaml +++ b/internal/resource/testdata/remote/controlplane-datasance.yaml @@ -30,10 +30,10 @@ systemMicroservices: 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.1 - arm64: ghcr.io/datasance/nats:2.14.2-rc.1 - riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 - arm: ghcr.io/datasance/nats:2.14.2-rc.1 + 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: diff --git a/internal/resource/testdata/remote/controlplane-iofog.yaml b/internal/resource/testdata/remote/controlplane-iofog.yaml index 99861f0db..e93295fbf 100644 --- a/internal/resource/testdata/remote/controlplane-iofog.yaml +++ b/internal/resource/testdata/remote/controlplane-iofog.yaml @@ -30,10 +30,10 @@ systemMicroservices: 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.1 - arm64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 - riscv64: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 - arm: ghcr.io/eclipse-iofog/nats:2.14.2-rc.1 + 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: diff --git a/internal/util/client/client_local_cp_sync_test.go b/internal/util/client/client_local_cp_sync_test.go index 16bb316b7..3fdfbabcf 100644 --- a/internal/util/client/client_local_cp_sync_test.go +++ b/internal/util/client/client_local_cp_sync_test.go @@ -47,7 +47,7 @@ func TestMergeRemoteAgentFromBackendPreservesSSHHostForLocalCPAgents(t *testing. Host: sshHost, SSH: rsc.SSH{User: "ubuntu", Port: 22, KeyFile: "/tmp/id_ed25519"}, Package: rsc.Package{ - Version: "v1.0.0-rc.3", + Version: "v1.0.0-rc.4", }, } diff --git a/internal/util/client/client_sync_test.go b/internal/util/client/client_sync_test.go index 0beb57ac2..1396dda94 100644 --- a/internal/util/client/client_sync_test.go +++ b/internal/util/client/client_sync_test.go @@ -18,7 +18,7 @@ func TestMergeRemoteAgentFromBackendPreservesSSHHost(t *testing.T) { SSH: rsc.SSH{User: "ubuntu2", Port: 32222, KeyFile: "/tmp/id_ed25519"}, Airgap: true, Package: rsc.Package{ - Version: "v1.0.0-rc.3", + Version: "v1.0.0-rc.4", }, } @@ -34,7 +34,7 @@ func TestMergeRemoteAgentFromBackendPreservesSSHHost(t *testing.T) { 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.3" { + if merged.SSH.User != "ubuntu2" || !merged.Airgap || merged.Package.Version != "v1.0.0-rc.4" { t.Fatalf("cached deploy metadata lost: %+v", merged) } if merged.UUID != "055bcc8d-91d2-445e-b7a2-f48e5bd98046" { diff --git a/pkg/iofog/install/edgelet_procedures_test.go b/pkg/iofog/install/edgelet_procedures_test.go index d7ac2e9ad..e9ca144f6 100644 --- a/pkg/iofog/install/edgelet_procedures_test.go +++ b/pkg/iofog/install/edgelet_procedures_test.go @@ -11,7 +11,7 @@ import ( func TestDefaultEdgeletProceduresScripts(t *testing.T) { util.SetEdgeletReleaseBaseForTest("https://github.com/Datasance/edgelet/releases/download") - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.3") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.4") t.Cleanup(func() { util.ResetEdgeletReleaseBaseForTest() util.ResetEdgeletBinaryVersionForTest() @@ -39,7 +39,7 @@ func TestDefaultEdgeletProceduresScripts(t *testing.T) { 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.3") { + if !strings.Contains(joined, "--version=v1.0.0-rc.4") { t.Fatalf("expected --version flag in install args, got %v", procs.Install.Args) } if !strings.Contains(joined, "--skip-start") { @@ -132,7 +132,7 @@ func TestInstallDepsSkipMatrix(t *testing.T) { func TestBootstrapCommandGeneration(t *testing.T) { util.SetEdgeletReleaseBaseForTest("https://example.com/download") - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.3") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.4") cfg := EdgeletInstallConfig{HostOS: "linux", Arch: "amd64", ContainerEngine: "docker"} procs, err := newDefaultEdgeletProcedures(EdgeletScriptStageDir, cfg) diff --git a/pkg/iofog/install/edgelet_remote_test.go b/pkg/iofog/install/edgelet_remote_test.go index 13e945be6..b20b66518 100644 --- a/pkg/iofog/install/edgelet_remote_test.go +++ b/pkg/iofog/install/edgelet_remote_test.go @@ -9,7 +9,7 @@ import ( func TestRemoteEdgeletBootstrapUsesMockedSSH(t *testing.T) { util.SetEdgeletReleaseBaseForTest("https://example.com/download") - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.3") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.4") t.Cleanup(func() { util.ResetEdgeletReleaseBaseForTest() util.ResetEdgeletBinaryVersionForTest() diff --git a/pkg/iofog/install/procedures_test.go b/pkg/iofog/install/procedures_test.go index 4ce2141e2..7d9dc0407 100644 --- a/pkg/iofog/install/procedures_test.go +++ b/pkg/iofog/install/procedures_test.go @@ -5,10 +5,10 @@ import "testing" func TestEntrypointGetCommandQuotesInstallArgs(t *testing.T) { ep := Entrypoint{ destPath: "/tmp/edgelet-scripts/install.sh", - Args: []string{"--version=v1.0.0-rc.3", "--arch=arm64", "--skip-start"}, + Args: []string{"--version=v1.0.0-rc.4", "--arch=arm64", "--skip-start"}, } got := ep.getCommand() - want := "/tmp/edgelet-scripts/install.sh '--version=v1.0.0-rc.3' '--arch=arm64' '--skip-start'" + want := "/tmp/edgelet-scripts/install.sh '--version=v1.0.0-rc.4' '--arch=arm64' '--skip-start'" if got != want { t.Fatalf("getCommand() = %q, want %q", got, want) } diff --git a/pkg/util/edgelet_binary_test.go b/pkg/util/edgelet_binary_test.go index d9176d665..37641bb91 100644 --- a/pkg/util/edgelet_binary_test.go +++ b/pkg/util/edgelet_binary_test.go @@ -46,13 +46,13 @@ func TestEdgeletBinaryArtifact(t *testing.T) { func TestEdgeletBinaryURL(t *testing.T) { edgeletReleaseBase = "https://github.com/Datasance/edgelet/releases/download" - edgeletBinaryVersion = "v1.0.0-rc.3" + edgeletBinaryVersion = "v1.0.0-rc.4" 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.3/edgelet-linux-amd64" + want := "https://github.com/Datasance/edgelet/releases/download/v1.0.0-rc.4/edgelet-linux-amd64" if got != want { t.Fatalf("EdgeletBinaryURL = %q, want %q", got, want) } @@ -80,7 +80,7 @@ func TestShouldSkipInstallDeps(t *testing.T) { 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.3/edgelet-linux-amd64" { + if r.URL.Path != "/v1.0.0-rc.4/edgelet-linux-amd64" { http.NotFound(w, r) return } @@ -89,7 +89,7 @@ func TestDownloadEdgeletBinary(t *testing.T) { defer server.Close() edgeletReleaseBase = server.URL - edgeletBinaryVersion = "v1.0.0-rc.3" + edgeletBinaryVersion = "v1.0.0-rc.4" dir := t.TempDir() dest := filepath.Join(dir, "edgelet-linux-amd64") diff --git a/versions.mk b/versions.mk index 6696014ae..c867e42db 100644 --- a/versions.mk +++ b/versions.mk @@ -1,6 +1,6 @@ OPERATOR_VERSION ?= 3.8.0-rc.1 -CONTROLLER_VERSION ?= 3.8.0-rc.2 +CONTROLLER_VERSION ?= 3.8.0-rc.4 ROUTER_VERSION ?= 3.8.0-rc.1 -NATS_VERSION ?= 2.14.2-rc.1 -EDGELET_BINARY_VERSION ?= v1.0.0-rc.3 +NATS_VERSION ?= 2.14.2-rc.2 +EDGELET_BINARY_VERSION ?= v1.0.0-rc.4 EDGELET_IMAGE_TAG ?= 1.0.0-rc.3 From c3cf0a0752c3baa1321432d26d719bb12f4ad254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 03:15:41 +0300 Subject: [PATCH 44/63] Update bats and func tests for retired CLI commands. Remove coverage for rename, legacy, and EdgeResource flows and simplify detach conflict checks to match the slimmer v3.8 command set. --- test/bats/common-k8s.bats | 34 +--------------- test/bats/local.bats | 33 +--------------- test/bats/vanilla.bats | 81 --------------------------------------- test/func/check.bash | 37 +----------------- 4 files changed, 6 insertions(+), 179 deletions(-) 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/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/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 9f26d027a..ddc5d9761 100755 --- a/test/func/check.bash +++ b/test/func/check.bash @@ -344,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}') ]] } @@ -404,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() { @@ -421,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 From a5558d6aba39c239f2d767213315818e1eaba420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 03:15:49 +0300 Subject: [PATCH 45/63] Add Lima VM fixtures for local remote control plane testing. Refresh remote control plane sample YAML for Lima-based integration runs and add VM and agent fixture files. --- .../testdata/remote/controlplane.yaml | 56 ++++++++++--------- .../resource/testdata/remote/lima-vm.yaml | 36 ++++++++++++ internal/resource/testdata/remote/remote.yaml | 23 ++++++++ 3 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 internal/resource/testdata/remote/lima-vm.yaml create mode 100644 internal/resource/testdata/remote/remote.yaml diff --git a/internal/resource/testdata/remote/controlplane.yaml b/internal/resource/testdata/remote/controlplane.yaml index 68032548c..334f113d5 100644 --- a/internal/resource/testdata/remote/controlplane.yaml +++ b/internal/resource/testdata/remote/controlplane.yaml @@ -10,11 +10,11 @@ spec: email: user@domain.com password: "TestPassword12!" controller: - publicUrl: https://192.168.139.85:51121 - consoleUrl: https://192.168.139.85:80 + publicUrl: https://192.168.105.2:51121 + consoleUrl: https://192.168.105.2:80 logLevel: info package: - image: docker.io/emirhandurmus/controller:3.8.0-rc.3 + image: docker.io/emirhandurmus/controller:3.8.0-rc.4 auth: mode: embedded insecureAllowHttp: false @@ -29,26 +29,27 @@ spec: 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.1 - arm64: ghcr.io/datasance/nats:2.14.2-rc.1 - riscv64: ghcr.io/datasance/nats:2.14.2-rc.1 - arm: ghcr.io/datasance/nats:2.14.2-rc.1 - routerSiteCA: - tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURPVENDQWlHZ0F3SUJBZ0lSQUtqeDNlUitiWEpmQ2pTSkZ6T3kxc2d3RFFZSktvWklodmNOQVFFTEJRQXcKR1RFWE1CVUdBMVVFQXhNT2NtOTFkR1Z5TFhOcGRHVXRZMkV3SGhjTk1qWXdNekV4TVRBME1ERTRXaGNOTXpFdwpNekV3TVRBME1ERTRXakFaTVJjd0ZRWURWUVFERXc1eWIzVjBaWEl0YzJsMFpTMWpZVENDQVNJd0RRWUpLb1pJCmh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBS2pwZlRHUVFJRFZRYTlRenAwQkxSZGI3TEZRS010eExiemUKY0hXeXAyUUp1VXZudm1LRG1rN3FuRkVYaVdpdFRLOVJPRCt6TWExMXF3M0lkb3ljNlhVZk42Y25UT1Z5YkE1bQp1QnZMQmV5eWhTcmVFYTV0dzVZVFNzbVhqRXJZek5LVzM0UWZIcmFvSTByYmhZL2Y5UXNwZUhXSkdiWlRiRlUwCis2N0lma0dCUlZGZUNRS3BEcEVMNVJ0cUpZOHFzanhXSlJ2NGYwaVE4UGYwUnhCSS9iZ0pBem42YS8vSVNFeUYKRkUxN2RBMXMzeGJsdFBPWU5CYll1bUtFd1lpTnRvSE9veWt1c2dodTFMOWVjU01WeHFlVjQ2Uk9BS0RRekdNTQphMXB0NVlNemVFNGZGcnhqY1l3SEhhSjN0ZEhCQkc4RWFmek5tOXk0a3Z2enZuK2JSUnNDQXdFQUFhTjhNSG93CkRnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQkJnZ3JCZ0VGQlFjREFqQVAKQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlNUVjd5K2xXQ2xNRytpRTJrTldrZjFQQWFzN3pBWgpCZ05WSFJFRUVqQVFnZzV5YjNWMFpYSXRjMmwwWlMxallUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFvQ2czCnY2a3U0YzBHYkIxV2ZXRjZnT3EzOXF2VFlmYmsxakpvdGxYTkl3dUowdTBxQ0IrTDFXYzN3MWNVM3NpZlNiSnoKY21VSWZ5Q2oyeFZRYUNGa1dDSkxtN0V0MXIrd0VYQ3V4VlJJSTRYcTdhSkVsM0w3WHFvU24rM1k4RDI1UjljLwo1OFQzNmpXcmhmVHdqWGs5Y3lDUTRxQnpkbmF5TGpqd3oyRU9BVnpFQWYyREhWZ2MrV1l2QVQxNmo5UndYMzFRCm9NWDc2azBCbHZtY2dzZGNlTTkrNmdHeGFEZnQrOUQyb05xdGNvMnhGWFBUVVRGalg4Mm05NXArbUk4UnBOMEsKRHloWFVheVo5anFKOGRORTU4TzZBUDJrelE4YWNwNHRQTGsrL3EwRjNmNnZEOGdVVHk3RWRwQiszekdFVWpwdApBcytrclNwTWV0REZRbEU0aEE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== - tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBcU9sOU1aQkFnTlZCcjFET25RRXRGMXZzc1ZBb3kzRXR2TjV3ZGJLblpBbTVTK2UrCllvT2FUdXFjVVJlSmFLMU1yMUU0UDdNeHJYV3JEY2gyakp6cGRSODNweWRNNVhKc0RtYTRHOHNGN0xLRkt0NFIKcm0zRGxoTkt5WmVNU3RqTTBwYmZoQjhldHFnalN0dUZqOS8xQ3lsNGRZa1p0bE5zVlRUN3JzaCtRWUZGVVY0SgpBcWtPa1F2bEcyb2xqeXF5UEZZbEcvaC9TSkR3OS9SSEVFajl1QWtET2Zwci84aElUSVVVVFh0MERXemZGdVcwCjg1ZzBGdGk2WW9UQmlJMjJnYzZqS1M2eUNHN1V2MTV4SXhYR3A1WGpwRTRBb05ETVl3eHJXbTNsZ3pONFRoOFcKdkdOeGpBY2RvbmUxMGNFRWJ3UnAvTTJiM0xpUysvTytmNXRGR3dJREFRQUJBb0lCQUFGckhJV3dYQlQ0NENQLwpFMkpzSW5CM2NYc05CNXFyRTZMcURKc0xGSzdFQ2lOTXRJNDlPVmJVK2krNnpvanJma3V4UWVpcHNqbHVWZHU0Ckd1UVVEcE1tTlBYRUN3MlkzV0Z0bEE3ZnNKSzJtUThDd3d2cVEySGM2RWJkd3BiVStwQ3Jld0Jhc1RaVmM4a1YKZU5TbXk4eFJNb0FYZ1BqRlVKRTlSYWtkRStVQms3dTJRc1R0QXNQNFlBUWJJMUdna0FDdGNLTUNaYnBSOXZyNQovVWc4bzJzSXBWMHJRL3F4ZVBqUGtwV3lWL25xV2hZRjI5MGN2bGVoZDBFQlVWOE1qWWduUlBuRmFsaXNaMm1iCjJzNldIZDAvMXNrVkdQS3ZsdTd0NEJVR2xMS09tSW00L09QVVRQRUJpd2liNWJycDZpVENQNkYwdWFQS09VQlIKa016VlB4RUNnWUVBMEREMmNMbyttVDhEMUIwSWxqWWx6L1UwekdKNXJaSkxzZ2F1UG5icmg2NU82RGhpUmtOTgpXSUpIQWVSNUdhV2tkOGZSS3JmRDhxUlZqOFppUGtZZDZoVjhMcHJUNHNuK1dwM1FFRE9IYlNQalYxRWdKRWYwCnU1QjFIZStjS0lVL0cvamRlVFF0RjRmNmp3bklyamhjVUs2MENMekJRcnJKbTZaRkhXaXdVZ01DZ1lFQXo3Tm0KNStWSUJpY3JsTGxQRU1aank5YUI5TkJGNFZsaEJEUDJYbVZOZnQrdE5LMUNwRWhKZVZscTJzWTdhVk5SVVpUaAp1OTJVNzQ3U2RGcHZQd1RDVkR3eER0SEhJOTB0NnRvYStqelBhMVN5cjk2aFhVejY4Ym1QaTFUTmhvQ3Z4L01yCm4xS2crNWtrNGZKV3pKd3gwUTNRdmMxTGtTM0lCdzJxWXpEeUlRa0NnWUJYalkvR1BuemU0NkpQak5vMG1aYnoKU3RLbWRXOW9jRkxIRG9vdW1NSmFjQktkRkVFMy9VdkV3aHpzamRIajJFWS9YVmY0bUFtZXZEK0RWRkd5a0xnNQoza2s0TEVLWmFJdEFQb2ZtbUZVR3NBWUdqWVp2MjVidlhrUHlqL2JqRDQ1SHpEUVBxY0tnMTcybWM5M2licTliCit1eVpsQS9PYVZFcDFSWFIxVm41VXdLQmdHY3lLZVQ2SklqNkdVc3hyemtVZVMwa0RUbkg2WkNIeWc0K2l5Qm4Ka05PQzZ4b0xJOXRnRnpGMTNnT0pEcWZNUDlFYStmVlBxTnBGeWdjSmo5QnQydWZqYURTR3dqenRmZ3o4QlA5awpDMksybUhtTlVmdDdiZ3VBT1BQdlZKYUpoYzBBNHlHcitsUkh5TzJDYk9JSWtTL2ZmMkZ1aVNjKzZlMm5Pb3RDCkhHdVJBb0dCQUpvZ280RXNBNnZBRSt6UVJ5T0pYaDJ1b2ZHaUZRSDF6VUladThSRTJCNHZmNE5IMEppZUtxVGEKQ2hvQ0luZzM2VlBQZUhxSjdWZUpXdVBXTnRobHdsV1QxM2FiQ2JrdzhKRVhjUGFpMnNYM3RhVjBLNEpMbFVkaApoeWhIeEgvQzB5clpEa2dmR2RFVlp4SjZ2emNBcUxlakVidHZuemFsSjNLRiszNzRnYk12Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== - routerLocalCA: - tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURWVENDQWoyZ0F3SUJBZ0lRVEhQRTdjWHlmM042cHc4TVJzc3MwekFOQmdrcWhraUc5dzBCQVFzRkFEQWkKTVNBd0hnWURWUVFERXhka1pXWmhkV3gwTFhKdmRYUmxjaTFzYjJOaGJDMWpZVEFlRncweU5qQXpNVEV4TURRdwpNVGhhRncwek1UQXpNVEF4TURRd01UaGFNQ0l4SURBZUJnTlZCQU1URjJSbFptRjFiSFF0Y205MWRHVnlMV3h2ClkyRnNMV05oTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF5d1Rjcy9kd0RXRWcKZS9OTGVPSDBjb0NsNEs0NFhwN0lPUkc2U0ZQVG9sQXJnWm56cUxMRnRKSThzblMwMnBlcDFRbFZXRWs4aU5Qcgp4UGc1M3VJeVJTWTc1MjRIb0oxQnlKaGxGa200SzVLWmYxbVFNQVd3VUpIQ0JlTG84VnZrd0Fjd2xYSWo0NUwvCkg0eTZSUXF3azMzTzFhVzMyVXJPcGZtVlVDQ1Z2MVROSElGY0UvTTdxVnN0Z1FYN3Zib2J6cVR1ZmZLMFNkM0sKTGFHVU9pbEVwRFI0T1hlalhrS1gyazZ2QTRLY1Mwb05NbnJ2UkN4ajA3L3NZeXE1Z0F1d1BDTkdRcWoyMDROaApKazl3NkF3cnZJUHhsN2IwcmRzaS84T1FONncvaFZjOGEyNFQ0ZlRpUVpBazllZVNHUUtkMkVvM3J1RytXdk5QCnZldzJua3JFN1FJREFRQUJvNEdHTUlHRE1BNEdBMVVkRHdFQi93UUVBd0lDcERBZEJnTlZIU1VFRmpBVUJnZ3IKQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVK0hXWApkMWxQNkNkVGttNWRtUUJzanowMHJxRXdJZ1lEVlIwUkJCc3dHWUlYWkdWbVlYVnNkQzF5YjNWMFpYSXRiRzlqCllXd3RZMkV3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUtNZlNQa0tZOGZidzdLd1VmTzBhZGNvYUYxdXlyYkwKVittdEd2YWJJbEVUNTJQQWYySm1pU25aM1dobHh0TWMveXNSRXZTcjBjMXcwek1LQ2FyWnBRQWRIS3UyN1ZhaApJcHRUZlp3TzFiRjhwcHRjVkZFbDI2Qyt2RmlPNzBMK2lhMWxGYnJTdHpTSUNwTEZYTjdBSS93akcvWjltVXBHClF4MUdpNGQybSs3ZmNVY2hiRzVpdVpUckkzaXFkRnJTWWJ0SlozdnhiRGhLT2YwalNwUG9Tdk96a3FwbnA2SXoKUFNMejhqeE8rYWljalk2MFY1WS9STldCNmNRd0F3UVBFa3F5aUJ3ZytxbXNlMWQrNi9YRmVoTEhtNVhjVFcweQoxNW91VGtjeWdPenZXWjE2VHZvTGdjWFZxVFgveEhLa0pJVklaWkpUN0srZnZ4T3VDQWFCRndBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== - tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBeXdUY3MvZHdEV0VnZS9OTGVPSDBjb0NsNEs0NFhwN0lPUkc2U0ZQVG9sQXJnWm56CnFMTEZ0Skk4c25TMDJwZXAxUWxWV0VrOGlOUHJ4UGc1M3VJeVJTWTc1MjRIb0oxQnlKaGxGa200SzVLWmYxbVEKTUFXd1VKSENCZUxvOFZ2a3dBY3dsWElqNDVML0g0eTZSUXF3azMzTzFhVzMyVXJPcGZtVlVDQ1Z2MVROSElGYwpFL003cVZzdGdRWDd2Ym9ienFUdWZmSzBTZDNLTGFHVU9pbEVwRFI0T1hlalhrS1gyazZ2QTRLY1Mwb05NbnJ2ClJDeGowNy9zWXlxNWdBdXdQQ05HUXFqMjA0TmhKazl3NkF3cnZJUHhsN2IwcmRzaS84T1FONncvaFZjOGEyNFQKNGZUaVFaQWs5ZWVTR1FLZDJFbzNydUcrV3ZOUHZldzJua3JFN1FJREFRQUJBb0lCQUV4UElBdnZLaW1GUS9vRApHVyt1OHJ4bE9iUkpsL3VNMERLUFJNY3g1djhBQmxKWkJScDRVOUxMRXRCN0NJMlBhekVkcUh3ZVR3Z1pLK29sCjZVNnJFLzBrNFdoY1ZiYWIxV0dxVW5pOXJlR0c0WFphT2xXcWxicTdCc1JDcFk4dkhMekhGdzVkVURzV2dobWcKUWxWNExxWEpxSWhxbVQrdUhCMkx3Z0ZUdGlXcmRWTnBGU0xLaUJLY2d5STNRWDJ2dGhvVWdmaURTY2NIRGxKKwp0OHMzczZ1MXdROW5jd2hMSGZ6dHVYREoxemhPS2kwTUIrYncyOTZBa1piclFtSE1VVjgzdWlQditQWkcybHozCnNLeTlIMTlraGxMVkh2TmpGVWRIREJ3cFdCR2RTZUREQkwrZUViTlFneHZHWmRkQ01hbzMrRjZkdG52SDVkMkMKS2JIUjc2c0NnWUVBNjFiTUxIRndkd0lkSmJIMm85ZXhmTytLTHhtQldYMUN5Y0JkK3RIZE9odHBwanNqYTVlbgpPSjdsTnU2a0c5ZGtnLzJuMTVsSWFYdEFPM0RkU00yZDlzNUplMXFxc3dCTmtvdjFPYk1CVjFaREpBYUN4UkU2CnFRMkxYSEphMTk0NVI3V3BRVGRKYldVWEY0OTJvdUMzaFpObFNOVFhqdHFEVnRubDB5L2xCVE1DZ1lFQTNOZXQKVll3anVKeTliWVF6SkpKRGxzaUJVeWFOdTdHNk5pSkxFQ3Fyb1pQa1gwMUU2UjBUTHJoWXZSNG0yOEltK0xyTApvWnNEZFZTS09tUURFY1hNUWJBa2ZUWVhMOVV2eUlFbWdIZ0Vsb0h6SjMrNEk5SlZqUXg3bWwzNkYxWUFRUy9UCi9ZM3p6MWZyNE5SZkV2RWxuWmFib1RCVG5ldW12UWdnc0hoTXpWOENnWUVBNm1mSTlCZUZtclFiVGhtRmZjcHcKZWUycDZLSHg2YTNQWVY3ZS9ONGVDU3VXdnNFMjFZcjNQM2xjKzZzVkFMbzQzeE0vSTRzRXlqTytWYlprWW9pVApaMm92WE5PQkpNd1BlQUU1bjJBQjNQa0o1UThySDVpNm9mbmdycE1ra3RGQW9vRjU5WUJZL2NKc0RzYVJ0MGcyCjQ3QmRlUDZ2T2hYQ0xqYlpLTklTdm1zQ2dZQWRuNVN4ci8ydXF0L0NEQVNzT0M1MjBHaUFsZUJYT0F6cGJBb3oKbmZXdDA5L0RaT01FZmhEdnFHekcyWCtPNU9sRFhoTW9sMW1NYUkydUxYSTM5UmRrREZPb3RCUENKOCtrRHFieQpmcWJtNVlHUFg5TjhncDlWTDBKNVAzZm5uM0tqUzk0YzJlakZmRjY0cHVRbDcxRURaWXQwd0wzR3BqQ1VsTDJGCnptMUc4d0tCZ0hQSUFhbURGQ1hlM0ZGNjloUWp0QTRCdUNRRko0UlVmWDhScFRadTJ1cE8zTWk1d0Q0bFQyMEMKOHFicy9lTUdCdDVOL3NLMEYrY1RoK0hEd3lTdVIzWHhmZGFyTlNKVytDOUdQVmN0ZVNHbjdpOEV3enpZNEt6NApPSzFseDZpaVdhQ2hLUGV1Sm43SHIwZ0h6VzVSaEZ4QjB1NnM3N2JCL1VVbFFlUGNaNlNmCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== - natsSiteCA: - tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURNakNDQWhxZ0F3SUJBZ0lRTmVmcTFGZVpOQWdTbStZSHltTnE1REFOQmdrcWhraUc5dzBCQVFzRkFEQVgKTVJVd0V3WURWUVFERXd4dVlYUnpMWE5wZEdVdFkyRXdIaGNOTWpZd016RXhNVEEwTVRNMldoY05NekV3TXpFdwpNVEEwTVRNMldqQVhNUlV3RXdZRFZRUURFd3h1WVhSekxYTnBkR1V0WTJFd2dnRWlNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDM3FobW91Um15OWFCMy95QTJQV1U3NytQUGFaZ1hYbmdpSk1mTDhEY3AKVXZmOEYwM20rSzhONzdBYzVlRTk5SFJpY1lMUUwyL2x6Q2h3VE5RNVBmaGp1NC9vaDlxMk5pSExXR21OeHBTbQo4aVRzV3FMS1VmUEVkZ2lDcmZNTTJOd1YyL1V0MjlKTXEzSzlYdTE1V0g3dnFIZUZQOGR1cWYrNEZTM1dlVFJkClZBdkovUi9KQWM5b3krTXpUWExpOXRmN1I5d2xNa2ZDQkJURUZHanpNRlpTZXZkY2hHcXk3Z284MUhCbm5qSXUKemc0NkNrUUVSbGE0eFpJdHlGVU5BRDNVNzlmejQvdVFZK3RCMGxOZytMVVk2K01pL0ZzK1BPSGlpVmw4eWMzUgpId3drMWpuNWFiV1V4NHZLZVExYVdyRFpxd3pJQ3RDMWdLalliNkdNeDBmTkFnTUJBQUdqZWpCNE1BNEdBMVVkCkR3RUIvd1FFQXdJQ3BEQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3RHdZRFZSMFQKQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVgxOXNQSVcxZFRJTG5GYnA0YW8rQUtCcUNNTXdGd1lEVlIwUgpCQkF3RG9JTWJtRjBjeTF6YVhSbExXTmhNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNaRXJ6VllzUUpDdnRlCnA4bVQ2RGlUNUhLTTJ1VmNnYkNzb2E0ck02bjRYYVp5bFAxMjZSZEJUSFhvWnNROE1pUi9rSmJHRkdBTEs5djcKTjB6ZUY3ZlFuUGV0b3NqWnVVV0pnYTF0aGdzRmZ4Yzg3TmJxSTRsN2FJVTFYa0FzN1ZObXZJRGJPYmZZVkoyVApPQkJXaWtvbkJ5aXVtOU9ncDUvT2FtRjFqVHRZS2duTGhUbzd4YlFqcksvdHJ2bWVuYXl4SWFlVWU2NHdNaFgwCmg2djJJTDQ3dnQ1b3lMOUJ4ZGcvaWorZlRLcXA1RnFMZ1dpbW5RUWxyMkhOTWlmMlljMWRKM3Nmb1EwaDZvSFMKRytpZm5aNjFHVjc1alh0ZVR6OGVPQ25oUDY5bnZKU2QvUTA3NHpvQUs1QXV1VTdFckVLYmdRQ0FEN3hxTTBlQwpqV3luNTJqcwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== - tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdDZvWnFMa1pzdldnZC84Z05qMWxPKy9qejJtWUYxNTRJaVRIeS9BM0tWTDMvQmROCjV2aXZEZSt3SE9YaFBmUjBZbkdDMEM5djVjd29jRXpVT1QzNFk3dVA2SWZhdGpZaHkxaHBqY2FVcHZJazdGcWkKeWxIenhIWUlncTN6RE5qY0ZkdjFMZHZTVEt0eXZWN3RlVmgrNzZoM2hUL0hicW4vdUJVdDFuazBYVlFMeWYwZgp5UUhQYU12ak0wMXk0dmJYKzBmY0pUSkh3Z1FVeEJSbzh6QldVbnIzWElScXN1NEtQTlJ3WjU0eUxzNE9PZ3BFCkJFWld1TVdTTGNoVkRRQTkxTy9YOCtQN2tHUHJRZEpUWVBpMUdPdmpJdnhiUGp6aDRvbFpmTW5OMFI4TUpOWTUKK1dtMWxNZUx5bmtOV2xxdzJhc015QXJRdFlDbzJHK2hqTWRIelFJREFRQUJBb0lCQUNYTHhzZUN2Zy9tdmR6cgozd1ppZUphT3piZ1ZrQ1BCQUdKd1pNQnFnUU9MVERhdjJndDk0bEp4SUxJMXVYWmRPK1UxWEZqNDVpTnBjZW40CldaVWxGRng3MFFmbWkwTURuVTFDTnNpakZOek5TSDF1UW9GMXYzOU8xZjRFaTVlNWVnTXlsb0JYTkM0c2V5cU8KNGpwZVZKTC92WWJwb3VJNmNFSkM4NEduUkRndk5jSjNLTEdMaE0xVVl3UWlubjd0QllCcDYrWjMvcExCNmRyYwp3SVhpT1A1blY3cWNjaDVwaGNaMktBVVhzYmNqcFluSEZrdE1sVlFkdWlQVHJ0UmdLcFBpMHVjRThqQkR5Ym5OCmtoMzd2elB1TUM5aFZSKzlodXBWNEoxbUNlQW05WVFqa0dobWcrM1gxZjh4d0JINFpPK1c4QXBuZUNhQ0VtdWEKbkN4b0NWa0NnWUVBN0NzTHRFZnk0VitvOE9WdDhVL1NaRE5VUWxBUkJhRzUwamhhSEtnd3hDQ3J2TWp1WHNteQpxdjd5UFRLc0s3YTBJVnBrUHliWldqdkI4ZVJybHBueWZPR2JOUDhJK3dlR0NWWmRaQXJkb1dCb25DYzU2MUs1CjNRaDFwMnJQVmd0TjdCS2Y4WEkzeVlsdkwyajI0SC9JRmhTNy81L0tjT0FOWUx3NmNoWG9LdVVDZ1lFQXh4WmYKUFZzamVJYzh0UnRzRTV2YXlBWEpHc3B6Tks2d3M1TnlTeVJhRlJiWU1Mc09GaVVVQlFiN0N1UFMwWVMyWUxEMQovZWtNcmtWWmZuWFpXSHZqeGFXLy9hL01ienhzYmdDUEJabkpoMUMyeW9lQnBMaDdUZ0p3dzJaaHdxOUl2VVVrCms4emZoclJMbUJLYldETG1RaHRIV2U1bzNHM3MzREpOUDdGN2tza0NnWUVBMmtUbWFsWmMyWUxweHNxa2svUXMKQk1PVHlqM3BuWVRkRXJkV1FVb0kyQnRCM2hidWg5aHVNcSt4L25HSXdsWDNvU1BEcHNJbSs4aGk5VWNoVUcwegp1Y3RoQU5mODJ0VVhRaVg1NW01TWE4dUlvMWwxcEZJdXlXUDZLU01FUVFmdG1wT1VFemgyNnVNRVNaTC9LSG13CjJRZU13VEpUallMbG1sUWN5RGdLL1NrQ2dZRUFnQUFLUzlDRkJjRXRidU9xb1JEYm9TN1hGYnFFUjZMcFNRdkwKdURRdkZ0QVJQNE9Fa3doVHpzZW1NR0k1OFN0NmRzQlA2R2dtRndYUGZGY1kzcU1JMXRLeWxkQ3BoL3M1VzZCUQpWREdFT05QVU1uTGRENkxzNUVMOWJTUXVScFdjRnRTVnA5RlpCYXAxejlobXVGWkJaTTlWR0tVSUZuRTJrSHhtCjNrU21Sc0VDZ1lFQW5yK01ZR0MzOFNuZVQ3dExSZm1QVEdEQXk4VWs2dm51ei9ySUN2YU4vSlNDbUFyS0wvSG8KQ091VHpOUUs4Y2twYkdQZGN1NnZWS1Y4bWJ6d0ZETCs4czRMZjhmRlBSajc2YVFiNEp3MTNTVHZlU2hSa2xaLwo1aGY4N3ZWWVd2SXkwQWovbExxdGdSY1p6RXpzWWdQQ0pGenlsU1Azb3BSVFg2emtYZ2V6NVpNPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= - natsLocalCA: - tlsCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVRENDQWppZ0F3SUJBZ0lSQVA4SlI0R0lYZm4wOUpKaWsrMkVIaDB3RFFZSktvWklodmNOQVFFTEJRQXcKSURFZU1Cd0dBMVVFQXhNVlpHVm1ZWFZzZEMxdVlYUnpMV3h2WTJGc0xXTmhNQjRYRFRJMk1ETXhNVEV3TkRFegpObG9YRFRNeE1ETXhNREV3TkRFek5sb3dJREVlTUJ3R0ExVUVBeE1WWkdWbVlYVnNkQzF1WVhSekxXeHZZMkZzCkxXTmhNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXNGSW1NZFJvU1VROFpOcTkKbDVRNnB0RWVub2hxMTdQWHIxMHdPVlZvd2J3VHBrUDZBdFgrMnhWMDNUbzF4SEoxdWZoOXczbWdKd2JaQW5iRAp3amdIU2ZmZkIyRjM2bWZjeVF4RGpZdTY2cGV6SVNzMlJNNVFwZEZhcTdoN0k5clIyNUorcGRPWnpzM29vUEt1CmFWWHpFUVVVUE1QNkxBem1pZndMVnAyUGFlZGZaRENxR1laL202eFA4cGtSSmdXc21RWmdWZVJ6WjdBMzNXSDkKRVFmTnc2WHZmN3NmQ0lvRUFVeFozaVZLd3hoMjJhaVpsRW03aTdYZkdLNVVBRFhVbG54V214UVFlM005dWNXMgprY21vbGNnKzA4dlc5bzFhSzdkNS91VmVNNExSbGVKSVF2R3c2YkFmVElNU0Q5L0gzdmg2M1diVis0N1krT2xtCkx6ZXJRUUlEQVFBQm80R0VNSUdCTUE0R0ExVWREd0VCL3dRRUF3SUNwREFkQmdOVkhTVUVGakFVQmdnckJnRUYKQlFjREFRWUlLd1lCQlFVSEF3SXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVNcjdyYmJ5VApBanNmVnVSQmI5Mm5SRDcvYkZVd0lBWURWUjBSQkJrd0Y0SVZaR1ZtWVhWc2RDMXVZWFJ6TFd4dlkyRnNMV05oCk1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ0hmbmdaeXJwMFJtaytMaG9qdkZrbVNyWlBkc0FFakpDWTNSSVgKTEVYZ2FzNTk5V3lERXJIaEo3MXNDTTdneWJGVXdKV3ZyOXliY2NZdVJqa2V5RE11bC9USlZjUERwc29zOGhrNgpOaElCa3pwRlZLdDRmL2t5NUpNMVJHdWxKRXdwNGNqazRMblJ0VFV5UGJpSm5XR3oybXBOaWFHVlI5bTluUW5QCk1mS0Q4dDJnZjdBV3pGS1poTWVRclZYMldmWHowKzRRUjJXck5NSjVnb0JHR1NCYTNrYU5LdE1ZS29BV25WYUEKME9FcGQ5VFNKZFlNUURwcVpTSVFhMEQya0Uvbi9PanNXTktyNGlzajVteEY2OU9vaHZhYm9YUFlORVI3YWl6OQpTc1B1Rmc2bjZLaXVYdkZnMGdFdE5Mb1dCZlBNaTZJOEhtY0MvdWlyNTlRU2JXZEYKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= - tlsKey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBc0ZJbU1kUm9TVVE4Wk5xOWw1UTZwdEVlbm9ocTE3UFhyMTB3T1ZWb3did1Rwa1A2CkF0WCsyeFYwM1RvMXhISjF1Zmg5dzNtZ0p3YlpBbmJEd2pnSFNmZmZCMkYzNm1mY3lReERqWXU2NnBleklTczIKUk01UXBkRmFxN2g3STlyUjI1SitwZE9aenMzb29QS3VhVlh6RVFVVVBNUDZMQXptaWZ3TFZwMlBhZWRmWkRDcQpHWVovbTZ4UDhwa1JKZ1dzbVFaZ1ZlUnpaN0EzM1dIOUVRZk53Nlh2ZjdzZkNJb0VBVXhaM2lWS3d4aDIyYWlaCmxFbTdpN1hmR0s1VUFEWFVsbnhXbXhRUWUzTTl1Y1cya2Ntb2xjZyswOHZXOW8xYUs3ZDUvdVZlTTRMUmxlSkkKUXZHdzZiQWZUSU1TRDkvSDN2aDYzV2JWKzQ3WStPbG1MemVyUVFJREFRQUJBb0lCQUE0cEJzcTlMNFBrMEVIYwpSRm9xVEJ5T0VscXlnM3dmeEJ4Z0RFbXFkNCtKaW4xeG02QkRMZVRMNEJjTlAvaXZKWS9DS2wxNnhOY2xpR09YCmhLaXlKYm0xeDZwWTFFL1Z1QWhJYlJ0dXc1dm9BM21RTmh0SUEzZVJyT202RnQrZUNQa01scEc4UU8rNEh5emYKMkl4Nm05cjcwTENCbjdPT2RLeFR1dGhocm4wWGRzODZIR21TTXpWWGFJZDBHeDA5LzV0c3BtZFBpWEpUWnluTQowR2lULzVHVHdWQUlLcEU4cjNRRjJnbWRqN2pBK3ZxT0V2eFNyYlcwRjVlT0hjRmRuMHBMUlNLWEVBRVVhaWNYCndGRVR4eWcrMHFtR3orcGZPWFQzbVFDU2kwbCtQSlRCdG1kVVZoQWg3cTdFODlEaWNHeW52UndGVXVKVGxheDUKTm5kUVVMMENnWUVBd2J3dUwwcFhnWXRuMDBLUThxN2gweGhnOVJZd0ZmTnREYzg2YSs3clRzcGlJTmZ5MDdQQwpyakhZS0dVejNZY1lFL1EzZGUxczQyUGVCM0NWaTJmeVBEZzJKSE1ySHpKOXFCY0pVam9GUzFQOHVRdlBjQlpxCmp2S1BYN0M1OFpKbXRkT0hsUXN0RG1OS3l3YjQ4czEveXhvQVh2M1ptN3V5RDFPZW9wWjYvQlVDZ1lFQTZQMHgKWDR6bEpRV0FIT0FPYWh0TnlLMVlYdWlZUUZOelRpOHVnQUZlbWdOdFJ3a1dCdWFNMENQTnZDZ0tDS3QxNEFHdgp3cHdMYlVuOFBBUXZWa2dRWlJneHhWdGZhU1kxNlIvYVJlRDJHVHZwSDc1YlRMWDd1SG5JaENMSUFDWnFtS1A2CkhvYWpFU0hHRi9ZaEtyV21SS0VReDhhVUpxMVRqV1FmQVFDVWdYMENnWUJXdUgyVC9ac2VDZUQzMkJ3NkJiNWcKVjlGTzVCZXlPN3pkS1ozbElwV0NOMldsZmdUY2J1TCtScUdUczNsNytEVDIrYUs1endXbTQ5VkhUMFlobU8zOQp0c3ZGbFNnQVZ3R1lkSGRmcjBrZlp3RUJkQi91OUpuT1V4V0twL2tVQVl5b1ozK1JYK2RUUVc4QllxV2RTZytpClFvbFgvQm1rZEdoSUpBNG1pV1dUNFFLQmdIWk1lTkZIUE9IN1ZQMVVWbjFSdDhENUl6R3RjQURaWG1hSVZsZncKV2hSaFFROGNjZTYzQ1RCMXZYU1g3K0JQRHQ3YWZGK1gwOFYrRjNCeHY0ZFR0OTljMVlpYnlHb2ZXS2d4NENZeQovMEg0eFhtMHNhN1ZpQ1kyejdVbjQ5MFBwSGcwYWo4dHBZYUJXNCszRFVnZVMzbjFQZ3Z4ckMrbk9oRkVrT2wxClhmSVJBb0dCQUlvb0ZyZ1pKelFHR3NEcGJ4VXJUVXBLK2xVekF0c0FFcWN6WnJWRm4rVlRFZ0dvMGRsdjRTVE0KK1lhRTdCUHpMbE1IeWZQdzVzMUpCbHVLVXIvcEpzOGZLWnlEaVVOcWR6S2NDZkJqNVJzd0NHV3IxbFZUN3RqQwpUeURiVFg3UmxWWVp0Y0FEVGxFa3kyYW9admN0OTIzbmEzM0R5V2VNUjBJUkg2S05VcXdLCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + 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: - ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlEUXpDQ0FpdWdBd0lCQWdJV0JBQ0RFWkpsWjVpRFpUT0lFUUlZT0RFakFaZ09OekFOQmdrcWhraUc5dzBCDQpBUXNGQURBYU1SZ3dGZ1lEVlFRREV3OTBaWE4wTFdOdmJuUnliMnhzWlhJd0hoY05Nall3TmpJek1Ua3hNekV4DQpXaGNOTWprd05qSXpNVFV5TWpRM1dqQWFNUmd3RmdZRFZRUURFdzkwWlhOMExXTnZiblJ5YjJ4c1pYSXdnZ0VpDQpNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNhV0R3aE42d2M3dUh0ZHZWZXB4T2FJMUNaDQo1d1NBWlJtbTJzMUlMajh3NUwxWllYT1BjTGFJNzhkRTM2dnlDN0VmM3JrL0dhV0cwMHJlampZWGtWakZxV0pLDQpGZENoelRadFkxK0tPSWM4UWJiVlpqanFmZjBPSHVDNnB3d3ZpQXZYNWQ0VmJmOXVPek8wS2pCMzdFR01KUHE2DQo4YWJuOU5mOXJVRHFDcm5OVlhJWEJQQllnd0hPOTlrdWZoNERmZ0R0V0dnYjVwa1J0Y2ZpOWkzdEd2YkFSaFk5DQo3TlQ3b1I2NVpBK3BNMndEQU1mSHVpNTZySTNkZUR1Ly9kQXdOUHM0UmFncFl4OWxld080MTByYmVESmlPbW1NDQo4SXBaNXZsaVdGdGxoa1BwVHV4M3JJTFB3Ym1ERDl1TkZRKzQwbjR0L3g3WEJzcjZsV2EzMjlndVRRWWJBZ01CDQpBQUdqZnpCOU1Bd0dBMVVkRXdFQi93UUNNQUF3RGdZRFZSMFBBUUgvQkFRREFnV2dNQjBHQTFVZEpRUVdNQlFHDQpDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFmQmdOVkhSRUVHREFXaHdUQXFJdFZnZzR4T1RJdU1UWTRMakV6DQpPUzQ0TlRBZEJnTlZIUTRFRmdRVVdoaVB6RE5VR3YwZzQ0V3pOY0FjdzFMVlBsQXdEUVlKS29aSWh2Y05BUUVMDQpCUUFEZ2dFQkFGTHN0amZmMFlPQmlGLy8vbkpVUXNDUld0dmxXVXBQQzBxOWE5d0ROL1pXUVMyZTZhMzVmYXJiDQp4N0dkUmFPVTMyUS9HTWpEUVJ3amVDZTlBNXZEZEpPN3ZFZWtPbjBnVEhScnVCTVYzbHNkVzZCZFhNQ3dtRG5TDQpBZlZja091TGpFRjYzQ1RsS1R6Vnp5QmFkZW1IVnFab2djTDdNSHNLZHFjZkNWVjlpYnlrcklzbVdCNWx2aG5iDQpJNjlId3l4Vk5OOHZWSEtuRmRqdVJHR0sybk5ubGNNTXIrR2NsNWFRUGlSTS82ME1EbzkyUXg4dzhjSTBGdzgrDQpqY0VvS3pTTzZ0VVk3NjlUVEF0eVowOFlCRllQbEk3RkkvbVkvckxNbU9Md21wL3hXSVFYeGlSNUNwbS9sUVBwDQozZldGUFlId2hOdjBsYzU0UjZQV2tNaDlKT0hnZWFNPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0K - cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlEUXpDQ0FpdWdBd0lCQWdJV0JBQ0RFWkpsWjVpRFpUT0lFUUlZT0RFakFaZ09OekFOQmdrcWhraUc5dzBCDQpBUXNGQURBYU1SZ3dGZ1lEVlFRREV3OTBaWE4wTFdOdmJuUnliMnhzWlhJd0hoY05Nall3TmpJek1Ua3hNekV4DQpXaGNOTWprd05qSXpNVFV5TWpRM1dqQWFNUmd3RmdZRFZRUURFdzkwWlhOMExXTnZiblJ5YjJ4c1pYSXdnZ0VpDQpNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNhV0R3aE42d2M3dUh0ZHZWZXB4T2FJMUNaDQo1d1NBWlJtbTJzMUlMajh3NUwxWllYT1BjTGFJNzhkRTM2dnlDN0VmM3JrL0dhV0cwMHJlampZWGtWakZxV0pLDQpGZENoelRadFkxK0tPSWM4UWJiVlpqanFmZjBPSHVDNnB3d3ZpQXZYNWQ0VmJmOXVPek8wS2pCMzdFR01KUHE2DQo4YWJuOU5mOXJVRHFDcm5OVlhJWEJQQllnd0hPOTlrdWZoNERmZ0R0V0dnYjVwa1J0Y2ZpOWkzdEd2YkFSaFk5DQo3TlQ3b1I2NVpBK3BNMndEQU1mSHVpNTZySTNkZUR1Ly9kQXdOUHM0UmFncFl4OWxld080MTByYmVESmlPbW1NDQo4SXBaNXZsaVdGdGxoa1BwVHV4M3JJTFB3Ym1ERDl1TkZRKzQwbjR0L3g3WEJzcjZsV2EzMjlndVRRWWJBZ01CDQpBQUdqZnpCOU1Bd0dBMVVkRXdFQi93UUNNQUF3RGdZRFZSMFBBUUgvQkFRREFnV2dNQjBHQTFVZEpRUVdNQlFHDQpDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFmQmdOVkhSRUVHREFXaHdUQXFJdFZnZzR4T1RJdU1UWTRMakV6DQpPUzQ0TlRBZEJnTlZIUTRFRmdRVVdoaVB6RE5VR3YwZzQ0V3pOY0FjdzFMVlBsQXdEUVlKS29aSWh2Y05BUUVMDQpCUUFEZ2dFQkFGTHN0amZmMFlPQmlGLy8vbkpVUXNDUld0dmxXVXBQQzBxOWE5d0ROL1pXUVMyZTZhMzVmYXJiDQp4N0dkUmFPVTMyUS9HTWpEUVJ3amVDZTlBNXZEZEpPN3ZFZWtPbjBnVEhScnVCTVYzbHNkVzZCZFhNQ3dtRG5TDQpBZlZja091TGpFRjYzQ1RsS1R6Vnp5QmFkZW1IVnFab2djTDdNSHNLZHFjZkNWVjlpYnlrcklzbVdCNWx2aG5iDQpJNjlId3l4Vk5OOHZWSEtuRmRqdVJHR0sybk5ubGNNTXIrR2NsNWFRUGlSTS82ME1EbzkyUXg4dzhjSTBGdzgrDQpqY0VvS3pTTzZ0VVk3NjlUVEF0eVowOFlCRllQbEk3RkkvbVkvckxNbU9Md21wL3hXSVFYeGlSNUNwbS9sUVBwDQozZldGUFlId2hOdjBsYzU0UjZQV2tNaDlKT0hnZWFNPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0K - key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ0KTUlJRW93SUJBQUtDQVFFQW1sZzhJVGVzSE83aDdYYjFYcWNUbWlOUW1lY0VnR1VacHRyTlNDNC9NT1M5V1dGeg0KajNDMmlPL0hSTityOGd1eEg5NjVQeG1saHROSzNvNDJGNUZZeGFsaVNoWFFvYzAyYldOZmlqaUhQRUcyMVdZNA0KNm4zOURoN2d1cWNNTDRnTDErWGVGVzMvYmpzenRDb3dkK3hCakNUNnV2R201L1RYL2ExQTZncTV6VlZ5RndUdw0KV0lNQnp2ZlpMbjRlQTM0QTdWaG9HK2FaRWJYSDR2WXQ3UnIyd0VZV1BlelUrNkVldVdRUHFUTnNBd0RIeDdvdQ0KZXF5TjNYZzd2LzNRTURUN09FV29LV01mWlhzRHVOZEsyM2d5WWpwcGpQQ0tXZWI1WWxoYlpZWkQ2VTdzZDZ5Qw0KejhHNWd3L2JqUlVQdU5KK0xmOGUxd2JLK3BWbXQ5dllMazBHR3dJREFRQUJBb0lCQUFKZC94SVhpSXBmYkhiUw0KSDc5ZGtmNUI0Zkg5OWRRSVNsK1FFWnp1dnRRdU1CcmtnMm44Uncrdk96Y2R0RnlpZkVWMS95aUZYWld1TU91Sw0KejlMdktqNG5xRGZDQnhPSlRHdUpXRkRSK1RmcGdURithQmNuZ2ljRkp6ZExkV2VkQ1drWnJub25CckFhRWJVUw0KaWpabUdNNjNxNytBaG5zMW5iYjZRU0pORDRvQWdXcklEYnBvdytCNitQY0lWMXcrQitpN3VvK2p1c0E1MVJKcQ0KMC8yLzcwQUhubXA2RlFaZi9lUldKR0hXZjNiUzNEck94VFlLdUNFYU1Tc04xcWczZ2lhNEgvU1dLL2E5ZGMxcg0KQmJvRllYVE5sZ2ZUTlM3SDNjTnM5OTNTdHdNbDIvd2J6QlV3c1hCV1hsZ2lnRnhhT1B4OTVTV2J6azZINDJVcw0KSFJVRmxjMENnWUVBMVhrb3lxWVYzZHlrUGFWMU80RkV3b3VPR3V5TVhYcHFqYTk3dmRQR3RtU3RhaERkYmx4SQ0KNzR3U3ZYb2Z4ZWNycnF0TlZ3NjdBS2VNNFY0aXhySzNhS1Y3UUdWL2RsZnlMN2tLRFlpZnBnTkdja2V0Tk80aw0KeUE5NmFQcFEzeGZCUWMvK091eFdZSDRJY3VVVC84WnFiSThPemhySzQ0U0x6S1NPTzZ0dDlhMENnWUVBdVJlWg0KTi85NDZTU1M3NEJTQWpWR3lPblJoUnlkMHM3cFI4WnRHZmhhbFlEZlplb2lkTHZwZWRLQzZEYkxXdkJrOEdZSw0KNjFYZllPcWlTclBMa3c1VlVZdTlUVmJUWHRJeTlvZUhFZUJEM3pudVRRVW9yb0lUOHZjek1iMWNWWW1MQkRYag0KbWdHclFRdzFtY2ExN2VuT2l0V1R3ZWo5b1dHaXVhZEE4MjVLaytjQ2dZQUw2SjVsNzYzNG1uNXZFZlBnUmUzWQ0KRElENDc0bEZEYmMwQUQ0ZThObDlBMURKUWZlWVdIMlpIMjlNTXF2akZtcFJiQ3o4Vms0SVUxQ0FvZ2UrbmVtdA0KWk0zalljWWlpL1Z1eEJ2VGRYT3loeXcrNDlDOXl5c3lIZXJ1UUVpU3FYaVdlMHZyYlpQRC9rUHFaTzBncjZqdw0KTldyV0JKaWM2S0FENG9vc3VmdUFZUUtCZ0JWYlhiaVNaOWN1K3kxYmR4citIcjdNQy9yNkJGUHd3QVlpSlRDYg0KOFlmU3FQUlBnYzVLYUhSQUVBN1BVOE9ZZlcwbnVSYlNmOFhsRFBqbHFoVzd6NmhySVZxdExCS0MycEtMck5BcA0KT211bGVaTzFocTRzSURVbXhPZDJYQk1hbmNuTWxnaU5MTCtDc3lTZFF4ekNuNnh4WEcxQmZ4S0IrNDdFZDhBZQ0KOThmOUFvR0JBS05oU29Ud1dxTE9SOXV1TkJNU2tIRlN6bm1JVXV1dXF4Zy9qMzFRRW1uRzJUbzhaR1kvQmN3Rw0KOXVXenhCY3dvUVBVUTFKeEdsTktrcXhUNnBYVDZIdVVBVHNaMnRIb2M0dkt1c1d3S2tOcFVycmRpMjdNMFhZSg0KNktiL3lMdTkva1ZPdjErazdscEFseXd4TVpjZ21MOVhsZWRjb3V4WmQzKzY1N1FsS2ZZUQ0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0NCg== + cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlETGpDQ0FoYWdBd0lCQWdJV0NBQmhPQkJBZFloM01tbDBGaUlnUlhVVEZvWU9OekFOQmdrcWhraUc5dzBCDQpBUXNGQURBUU1RNHdEQVlEVlFRREV3VnBiMlp2WnpBZUZ3MHlOakEyTWpRd09UTTVNREJhRncweU9UQTJNalF3DQpOVFE0TXpaYU1CQXhEakFNQmdOVkJBTVRCV2x2Wm05bk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBDQpNSUlCQ2dLQ0FRRUFxYlRjZFJmdU1iYmNHQUpNK0NDRk5JU1lKNDhpMENhUURRMWhDRmFtc2I3K0pmMElScml0DQpaTCtsb2d2a0Q4RW5SUWNhS0RmMkkvNDljNkF1YiszSERkMTJCa2N1NGRUNEQ0blduazVJNHJXakxRMGdzSGRaDQpNS3YwMVpUUjE4SG5JNTJaOWtLYnBtaFJiei9SSitWeGpqb2llWDVaTkplZVNwL2JIYk9TeUFROFhFSlA1MEZ4DQpQVHo3SktwUFN3bHE2dDBhWDVFUy9XajNuNXNhQTYybzBpV01vbWFVaTRGeG95RUljekJsbWd4RE9BdEM2UjV6DQpFbkcyeExJcklFWVBCUmJBWlIyNnU5N0lGTWI0eUhnYWJJSkNUM3JWUDYzYkdabmNlM0lhaUUrSzRoZDN5K2wwDQp5RVRPdzMxY2pHa0wwSTEyVzlKY1E2MDNrcStRUVUrK3dRSURBUUFCbzM0d2ZEQU1CZ05WSFJNQkFmOEVBakFBDQpNQTRHQTFVZER3RUIvd1FFQXdJRm9EQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3DQpIZ1lEVlIwUkJCY3dGWWNFd0tocEFvSU5NVGt5TGpFMk9DNHhNRFV1TWpBZEJnTlZIUTRFRmdRVWZTWWJQZnJQDQprZ3NabmlFaUdBeFJGUGdZYlBnd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFZnkzeSt0ZGtnTXVxa3FQZDdpDQoxNWk5elQxeC9ybm5uVjIvRDYxVy9rLzVsZWhSMERRNkxqbWxoNERaMi81b1YxY3JwVHZWUkw5R0dCRUtNdUM1DQo4dzk5NWY3WEQ2eWVabHAwdkJhSlo0OXVHQ1FGMGc5K09kSkNIeHVETU1zaWcxRDk4ZnJYV2tjZkZ4OW9VcW1SDQptQi80VkVCdkt6SVlVREJwZ0lQeWozV24zR1g5WVFtM3J1TjlmNVBvTUsyeHpXVVE3SW5OMjF6WTV4Q2dVajBhDQo1OEhjc002SWdXbW4xU0FaY0h2bGZpaGgvbmtIY3ZDVXA0VzJEeFRNQXFxM2xNSWIwTS9JOWxaYXZaMi8ySUlZDQpFbE1kSkRCdjd6OFUveElnbFZvaWlSM3p6Yjd4T25ZWnRiS0dNQlVIYThoeVI1ekZaWGRrQ25rRFpPc2gxZzh1DQpReDg9DQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tDQo= + key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ0KTUlJRW93SUJBQUtDQVFFQXFiVGNkUmZ1TWJiY0dBSk0rQ0NGTklTWUo0OGkwQ2FRRFExaENGYW1zYjcrSmYwSQ0KUnJpdFpMK2xvZ3ZrRDhFblJRY2FLRGYySS80OWM2QXViKzNIRGQxMkJrY3U0ZFQ0RDRuV25rNUk0cldqTFEwZw0Kc0hkWk1LdjAxWlRSMThIbkk1Mlo5a0ticG1oUmJ6L1JKK1Z4ampvaWVYNVpOSmVlU3AvYkhiT1N5QVE4WEVKUA0KNTBGeFBUejdKS3BQU3dscTZ0MGFYNUVTL1dqM241c2FBNjJvMGlXTW9tYVVpNEZ4b3lFSWN6QmxtZ3hET0F0Qw0KNlI1ekVuRzJ4TElySUVZUEJSYkFaUjI2dTk3SUZNYjR5SGdhYklKQ1QzclZQNjNiR1puY2UzSWFpRStLNGhkMw0KeStsMHlFVE93MzFjakdrTDBJMTJXOUpjUTYwM2txK1FRVSsrd1FJREFRQUJBb0lCQUQ1c0FqTW56RHVKRVVmYg0KZ01nNzNnTkZTbG95c2hGeVBjWXZSNk96aTdrUmtaWVRqbm5FOERLQXM4SDVNYmdCeWhuLzFNVTZZRlU0N1EyYw0KdTdmNzlCM0xlZUF4U3JOU2pMUGFkWkRoSnJvTkthb21qQUdjeExlOGFHQXZUMGhYVUZldlhyUlFKOFI3MW9oZg0KSnVYUDVZYjFKejBkRmw3YjdpTnd6VDROa1UzMGQ0L3FqUFZGekd0aUp1bGcrK1pSeU4vUURKa3YvR2xVdEdTNA0KVnE4ZVNpdHQyRWY2TE42RFJmN3BKUXE3M1J5bTU2aGtVd2tRVS8vWllwZHo2ZGYzbHZOM0p6cFlBUm80aytCMg0Kend0cHMzdWRtaTRvMHEvVDVNSS96azI5WGVONkc2Z2pDWkZaQU13TlNzNlFWSFdDVDlvbDZPUWJVM1ZmL0ZoTA0KaHV2ZFI4a0NnWUVBMGl3ZU9hN3VYNUJhZmtyQWdKUW8vRVVQVjVLenpiN1M3cVVpNjNMTlVmTHlkVFlrL3B5MQ0KTUwvcHVoN1M0TE9jb05tL0doRFZwSG9RTWs2bHpwSTcyL2hUaTJVRXZ0RHFXR2t0NGhyWDZ2R2NwUERDSWwydg0KcE5EYlNzZkJiZXJ1ME9IbHdRRHl5N1lpaEJ5RHpjOGtzcHJnNjJNa21uc01xd1NyMGF1RldqTUNnWUVBenJYdA0KY3c5Uy9NTlpuZmYzNVo0eklGSVJWenJyM2NYVDJGTWkveG55SlVmVFNIZUNINlNBSUNmT3Zvb01tU0xNY3B1dQ0KRHdDN01Sb3VwT0tPUDZ1Y2ZNMnpobC9WRU9ENXhDdmJNUEw4Y01HaVgxL1dKaVNOTUxmSFZjK3Y5TUduclAvag0KdEZNd0ViL1BBSmR2eVlMRDRKdFVNbGEwTFFrdUVpdHNEMk1CTnpzQ2dZRUF6cUR4Vm1UVmxyNmxPV1RrdllUcw0KaHBpZTdNb2VYRGt2eDlBeTlLaDVsQWYydDZYejJSN1lSSVZwbE1LWk1MRmxXLzY0RXpoWjBzcnZBWWF4SE5aSQ0KWTR0UkY4ckpUNUMxMVJZVE5paU4vejVyY3Y0QTN5aWNkcjJmMjNWb2hsaGpVcG5FK0d1bVNRRllEZXE2NnF5cw0KdDZ2dFZYNnpqZTFNRnhjUWxhSzhDR0VDZ1lBeTg1RDEzWDhkSXFIQ1dFN1lZR2hGdlRUZEJYOENDVE13alQyQg0KRjhvaCtsUlA5blV5aTMreGJWNStoTlNhSW9PMmREMHhJWU1DbFd1TjVQSWZLNVBGQjRjS3hqQmttcSsxOVFGdw0KVFZTQURwcVJXN1FUYnNzR2lTWXZOcVF1ZGxWNFJSTEJiZ2ZaT0NnMEF4L2cwY1NxWmw4WWtWcVVCMEU5NVVvYg0KZU5IaDVRS0JnSFJpQkZaYlc2cHBQYjFWa25jVXVaNXlCUjNkQzhnQzJGZWFqY2toMUtTVFBxeDlYdk9KUXNidQ0KTmx4YUVQbmpxci9LN3Jmd3ExdWRRL0tHNnUxVjNuUWZRUFBGTW85RU5UY3NGbDJoc2dWYk5naGVGV0xsQlA3ZA0KLzRXVDd5cnlja1hkK2FaZ0trWGE2aVE0TFA1ciswM0ppMTd0aG5FcUVDb3hKb1Y5QXg5eg0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0NCg== + ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlETGpDQ0FoYWdBd0lCQWdJV0NBQmhPQkJBZFloM01tbDBGaUlnUlhVVEZvWU9OekFOQmdrcWhraUc5dzBCDQpBUXNGQURBUU1RNHdEQVlEVlFRREV3VnBiMlp2WnpBZUZ3MHlOakEyTWpRd09UTTVNREJhRncweU9UQTJNalF3DQpOVFE0TXpaYU1CQXhEakFNQmdOVkJBTVRCV2x2Wm05bk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBDQpNSUlCQ2dLQ0FRRUFxYlRjZFJmdU1iYmNHQUpNK0NDRk5JU1lKNDhpMENhUURRMWhDRmFtc2I3K0pmMElScml0DQpaTCtsb2d2a0Q4RW5SUWNhS0RmMkkvNDljNkF1YiszSERkMTJCa2N1NGRUNEQ0blduazVJNHJXakxRMGdzSGRaDQpNS3YwMVpUUjE4SG5JNTJaOWtLYnBtaFJiei9SSitWeGpqb2llWDVaTkplZVNwL2JIYk9TeUFROFhFSlA1MEZ4DQpQVHo3SktwUFN3bHE2dDBhWDVFUy9XajNuNXNhQTYybzBpV01vbWFVaTRGeG95RUljekJsbWd4RE9BdEM2UjV6DQpFbkcyeExJcklFWVBCUmJBWlIyNnU5N0lGTWI0eUhnYWJJSkNUM3JWUDYzYkdabmNlM0lhaUUrSzRoZDN5K2wwDQp5RVRPdzMxY2pHa0wwSTEyVzlKY1E2MDNrcStRUVUrK3dRSURBUUFCbzM0d2ZEQU1CZ05WSFJNQkFmOEVBakFBDQpNQTRHQTFVZER3RUIvd1FFQXdJRm9EQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3DQpIZ1lEVlIwUkJCY3dGWWNFd0tocEFvSU5NVGt5TGpFMk9DNHhNRFV1TWpBZEJnTlZIUTRFRmdRVWZTWWJQZnJQDQprZ3NabmlFaUdBeFJGUGdZYlBnd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFZnkzeSt0ZGtnTXVxa3FQZDdpDQoxNWk5elQxeC9ybm5uVjIvRDYxVy9rLzVsZWhSMERRNkxqbWxoNERaMi81b1YxY3JwVHZWUkw5R0dCRUtNdUM1DQo4dzk5NWY3WEQ2eWVabHAwdkJhSlo0OXVHQ1FGMGc5K09kSkNIeHVETU1zaWcxRDk4ZnJYV2tjZkZ4OW9VcW1SDQptQi80VkVCdkt6SVlVREJwZ0lQeWozV24zR1g5WVFtM3J1TjlmNVBvTUsyeHpXVVE3SW5OMjF6WTV4Q2dVajBhDQo1OEhjc002SWdXbW4xU0FaY0h2bGZpaGgvbmtIY3ZDVXA0VzJEeFRNQXFxM2xNSWIwTS9JOWxaYXZaMi8ySUlZDQpFbE1kSkRCdjd6OFUveElnbFZvaWlSM3p6Yjd4T25ZWnRiS0dNQlVIYThoeVI1ekZaWGRrQ25rRFpPc2gxZzh1DQpReDg9DQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tDQo= + nats: enabled: true events: @@ -57,18 +58,19 @@ spec: cleanupInterval: 86400 captureIpAddress: true controllers: - - name: remote-1 + - name: controlplane host: 0.0.0.0 ssh: - user: ubuntu - keyFile: /Users/emirhan/.orbstack/ssh/id_ed25519 - port: 32222 + user: emirhan + keyFile: /Users/emirhan/.lima/_config/user + port: 50367 systemAgent: config: - host: 192.168.139.85 + host: 192.168.105.2 arch: arm64 containerEngine: edgelet deploymentType: native + networkInterface: dynamic # - name: remote-2 # host: 192.168.139.148 # ssh: 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..c63ab2ef5 --- /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: 60112 + airgap: true + # package: + # container: + # image: ghcr.io/datasance/edgelet:1.0.0-rc.3 + config: + host: 192.168.105.3 + networkInterface: lima0 + logLevel: INFO + deploymentType: native + containerEngine: edgelet + # containerEngineUrl: unix:///var/run/docker.sock + arch: arm64 \ No newline at end of file From 6a683e6582cd54b328e24771d18b28ec0ccc11b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 11:36:43 +0300 Subject: [PATCH 46/63] Wait for async agent and service platform provisioning after deploy. Add WaitForAgentPlatformReady and WaitForServiceProvisioningReady with 400s timeout and one automatic reconcile retry on failure. Expose iofogctl reconcile agent|service for manual retries. Surface platformStatus on describe agent status and fix service SDK type handling. --- internal/cmd/reconcile.go | 60 ++++++++++ internal/cmd/root.go | 1 + internal/deploy/agentconfig/factory.go | 19 +++- internal/deploy/service/factory.go | 18 ++- .../describe/agent_platform_status_test.go | 49 ++++++++ internal/describe/service.go | 9 +- internal/describe/utils.go | 40 +++++++ internal/get/services.go | 2 +- internal/reconcile/agent/execute.go | 27 +++++ internal/reconcile/service/execute.go | 33 ++++++ .../testdata/remote/controlplane.yaml | 10 +- internal/resource/testdata/remote/remote.yaml | 2 +- internal/resource/types.go | 9 +- internal/util/client/api.go | 1 + internal/util/client/platform.go | 105 ++++++++++++++++++ internal/util/client/platform_test.go | 34 ++++++ pkg/iofog/install/container_engine.go | 2 +- 17 files changed, 398 insertions(+), 23 deletions(-) create mode 100644 internal/cmd/reconcile.go create mode 100644 internal/describe/agent_platform_status_test.go create mode 100644 internal/reconcile/agent/execute.go create mode 100644 internal/reconcile/service/execute.go create mode 100644 internal/util/client/platform.go create mode 100644 internal/util/client/platform_test.go diff --git a/internal/cmd/reconcile.go b/internal/cmd/reconcile.go new file mode 100644 index 000000000..4315e5fc4 --- /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: "iofogctl 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: "iofogctl 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/root.go b/internal/cmd/root.go index 07a6ed6c5..08bf61c0d 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -88,6 +88,7 @@ func NewRootCommand() *cobra.Command { newStopCommand(), newMoveCommand(), newRebuildCommand(), + newReconcileCommand(), newDockerPruneCommand(), newUpgradeCommand(), newRollbackCommand(), diff --git a/internal/deploy/agentconfig/factory.go b/internal/deploy/agentconfig/factory.go index 14553ecbf..6eea4414a 100644 --- a/internal/deploy/agentconfig/factory.go +++ b/internal/deploy/agentconfig/factory.go @@ -184,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/service/factory.go b/internal/deploy/service/factory.go index ae8e8847b..a1224d281 100644 --- a/internal/deploy/service/factory.go +++ b/internal/deploy/service/factory.go @@ -12,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 @@ -51,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) { @@ -69,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 { @@ -80,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/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/service.go b/internal/describe/service.go index c967979e0..e3b525afa 100644 --- a/internal/describe/service.go +++ b/internal/describe/service.go @@ -7,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 @@ -58,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/utils.go b/internal/describe/utils.go index c05e0a8b4..c18f757b4 100644 --- a/internal/describe/utils.go +++ b/internal/describe/utils.go @@ -309,6 +309,9 @@ 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 @@ -396,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/get/services.go b/internal/get/services.go index 94dc50e94..647a5c0cb 100644 --- a/internal/get/services.go +++ b/internal/get/services.go @@ -86,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/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/resource/testdata/remote/controlplane.yaml b/internal/resource/testdata/remote/controlplane.yaml index 334f113d5..83b340a1f 100644 --- a/internal/resource/testdata/remote/controlplane.yaml +++ b/internal/resource/testdata/remote/controlplane.yaml @@ -12,12 +12,12 @@ spec: controller: publicUrl: https://192.168.105.2:51121 consoleUrl: https://192.168.105.2:80 - logLevel: info + logLevel: debug package: - image: docker.io/emirhandurmus/controller:3.8.0-rc.4 + image: ghcr.io/datasance/controller:3.8.0-rc.4 auth: mode: embedded - insecureAllowHttp: false + insecureAllowHttp: true insecureAllowBootstrapLog: false bootstrap: username: admin @@ -63,14 +63,14 @@ spec: ssh: user: emirhan keyFile: /Users/emirhan/.lima/_config/user - port: 50367 + port: 57884 systemAgent: config: host: 192.168.105.2 arch: arm64 containerEngine: edgelet deploymentType: native - networkInterface: dynamic + networkInterface: lima0 # - name: remote-2 # host: 192.168.139.148 # ssh: diff --git a/internal/resource/testdata/remote/remote.yaml b/internal/resource/testdata/remote/remote.yaml index c63ab2ef5..f32cbc790 100644 --- a/internal/resource/testdata/remote/remote.yaml +++ b/internal/resource/testdata/remote/remote.yaml @@ -8,7 +8,7 @@ spec: ssh: user: emirhan keyFile: /Users/emirhan/.lima/_config/user - port: 60112 + port: 57875 airgap: true # package: # container: diff --git a/internal/resource/types.go b/internal/resource/types.go index b15aa5356..db6a58409 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -213,10 +213,11 @@ type AgentStatus struct { IsReadyToRollback bool `json:"isReadyToRollback" yaml:"isReadyToRollback"` Tunnel string `json:"tunnel" yaml:"tunnel"` VolumeMounts []VolumeMount - GpsStatus string `json:"gpsStatus" yaml:"gpsStatus"` - AvailableRuntimes []string `json:"availableRuntimes" yaml:"availableRuntimes"` - RuntimeAgentPhase string `json:"runtimeAgentPhase" yaml:"runtimeAgentPhase"` - ControlPlaneQuiesced bool `json:"controlPlaneQuiesced" yaml:"controlPlaneQuiesced"` + 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"` } // ArchStringToID maps canonical architecture names to Controller archId values. diff --git a/internal/util/client/api.go b/internal/util/client/api.go index 85c487ae4..83e5b7678 100644 --- a/internal/util/client/api.go +++ b/internal/util/client/api.go @@ -279,6 +279,7 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura AvailableRuntimes: []string(agentInfo.AvailableRuntimes), RuntimeAgentPhase: agentInfo.RuntimeAgentPhase, ControlPlaneQuiesced: agentInfo.ControlPlaneQuiesced, + PlatformStatus: agentInfo.PlatformStatus, } return agentConfig, tags, agentStatus, err 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/pkg/iofog/install/container_engine.go b/pkg/iofog/install/container_engine.go index 8250063cc..adb071c07 100644 --- a/pkg/iofog/install/container_engine.go +++ b/pkg/iofog/install/container_engine.go @@ -8,7 +8,7 @@ import ( dockengine "github.com/eclipse-iofog/iofogctl/pkg/containerengine/docker" ) -// DefaultLocalContainerEngine is the host runtime used for legacy LocalController docker deploy. +// 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. From 3791032eee4d3debb660ac451388477ce854c5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 15:22:59 +0300 Subject: [PATCH 47/63] Add validated file I/O helpers and verify SSH host keys. Introduce shared permission constants, root-scoped path reads, and host key pinning. Replace InsecureIgnoreHostKey and tighten trust store file modes. --- internal/config/config.go | 12 ++- internal/config/namespace.go | 4 +- internal/config/permissions.go | 82 +++++++++++++++ internal/trust/store.go | 13 +-- internal/trust/store_test.go | 3 +- pkg/containerengine/docker/container.go | 2 +- pkg/containerengine/docker/copy.go | 45 +++++--- pkg/containerengine/docker/image.go | 2 +- pkg/util/exec.go | 4 +- pkg/util/iohelpers.go | 43 ++++++++ pkg/util/permissions.go | 10 ++ pkg/util/rand.go | 22 ---- pkg/util/safepath.go | 132 ++++++++++++++++++++++++ pkg/util/ssh.go | 10 +- pkg/util/ssh_hostkey.go | 111 ++++++++++++++++++++ pkg/util/ssh_hostkey_test.go | 99 ++++++++++++++++++ pkg/util/yaml.go | 4 +- 17 files changed, 537 insertions(+), 61 deletions(-) create mode 100644 internal/config/permissions.go create mode 100644 pkg/util/iohelpers.go create mode 100644 pkg/util/permissions.go delete mode 100644 pkg/util/rand.go create mode 100644 pkg/util/safepath.go create mode 100644 pkg/util/ssh_hostkey.go create mode 100644 pkg/util/ssh_hostkey_test.go diff --git a/internal/config/config.go b/internal/config/config.go index 972a2fad1..21fe9cbca 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -72,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 @@ -81,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) @@ -96,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 @@ -191,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 } @@ -206,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 } @@ -219,6 +221,8 @@ func Flush() error { } // 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 } diff --git a/internal/config/namespace.go b/internal/config/namespace.go index 64b014693..063264866 100644 --- a/internal/config/namespace.go +++ b/internal/config/namespace.go @@ -105,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 } @@ -141,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 } 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/trust/store.go b/internal/trust/store.go index d7f7df6d1..bf0280f43 100644 --- a/internal/trust/store.go +++ b/internal/trust/store.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/eclipse-iofog/iofogctl/pkg/util" ) var ErrNotFound = errors.New("trust CA not found") @@ -64,14 +65,14 @@ func StoreCA(namespace, caBase64 string) error { if err != nil { return err } - if err := os.MkdirAll(dir, 0700); err != nil { + if err := os.MkdirAll(dir, util.DirPerm); err != nil { return err } path, err := caPath(namespace) if err != nil { return err } - if err := os.WriteFile(path, pem, 0600); err != nil { + if err := util.WriteValidatedFile(path, pem, util.FilePerm); err != nil { return err } return SetCachedMode(namespace, ModeNamespace) @@ -83,7 +84,7 @@ func GetCA(namespace string) ([]byte, error) { if err != nil { return nil, err } - data, err := os.ReadFile(path) + data, err := util.ReadValidatedFile(path) if err != nil { if os.IsNotExist(err) { return nil, ErrNotFound @@ -122,7 +123,7 @@ func GetCachedMode(namespace string) (Mode, bool) { if err != nil { return "", false } - data, err := os.ReadFile(path) + data, err := util.ReadValidatedFile(path) if err != nil { return "", false } @@ -141,12 +142,12 @@ func SetCachedMode(namespace string, mode Mode) error { if err != nil { return err } - if err := os.MkdirAll(dir, 0700); err != nil { + if err := os.MkdirAll(dir, util.DirPerm); err != nil { return err } path, err := modePath(namespace) if err != nil { return err } - return os.WriteFile(path, []byte(string(mode)), 0600) + return util.WriteValidatedFile(path, []byte(string(mode)), util.FilePerm) } diff --git a/internal/trust/store_test.go b/internal/trust/store_test.go index 575877a41..ebcef9f09 100644 --- a/internal/trust/store_test.go +++ b/internal/trust/store_test.go @@ -2,6 +2,7 @@ package trust import ( "encoding/base64" + "errors" "path/filepath" "testing" @@ -43,7 +44,7 @@ func TestGetCA_NotFound(t *testing.T) { config.Init(dir) _, err := GetCA("missing") - if err != ErrNotFound { + if !errors.Is(err, ErrNotFound) { t.Fatalf("err = %v", err) } } diff --git a/pkg/containerengine/docker/container.go b/pkg/containerengine/docker/container.go index fd2fc1c84..0520b16e1 100644 --- a/pkg/containerengine/docker/container.go +++ b/pkg/containerengine/docker/container.go @@ -276,7 +276,7 @@ func (c *Client) ExecuteCmd(name string, cmd []string) (ExecResult, error) { defer attachResp.Close() var outBuf, errBuf bytes.Buffer - if _, err = stdcopy.StdCopy(&outBuf, &errBuf, attachResp.Reader); err != nil && err != io.EOF { + if _, err = stdcopy.StdCopy(&outBuf, &errBuf, attachResp.Reader); err != nil && !errors.Is(err, io.EOF) { return result, err } diff --git a/pkg/containerengine/docker/copy.go b/pkg/containerengine/docker/copy.go index 76396e18c..c09dfdc71 100644 --- a/pkg/containerengine/docker/copy.go +++ b/pkg/containerengine/docker/copy.go @@ -6,6 +6,7 @@ import ( "compress/gzip" "fmt" "io" + "io/fs" "os" "path/filepath" "time" @@ -34,35 +35,45 @@ func (c *Client) CopyToContainer(name, source, dest string) error { } 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) - srcLength := len(filepath.ToSlash(src)) - err := filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { - if err != nil { - return err + err = fs.WalkDir(root.FS(), ".", func(rel string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr } - if file == src { + if rel == "." { return nil } - header, err := tar.FileInfoHeader(fi, file) + info, err := entry.Info() if err != nil { return err } - name := string([]rune(filepath.ToSlash(file))[srcLength:]) - header.Name = name + 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 !fi.IsDir() { - data, err := os.Open(file) - if err != nil { - return err - } - defer data.Close() - if _, err := io.Copy(tw, data); 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 }) diff --git a/pkg/containerengine/docker/image.go b/pkg/containerengine/docker/image.go index a5fdad1ce..522374618 100644 --- a/pkg/containerengine/docker/image.go +++ b/pkg/containerengine/docker/image.go @@ -28,7 +28,7 @@ func (c *Client) PullImage(image string, opts PullOptions) error { Username: opts.Username, Password: opts.Password, } - authJSON, err := json.Marshal(authConfig) + authJSON, err := json.Marshal(authConfig) // #nosec G117 -- Moby registry auth JSON required by Docker API if err != nil { return err } diff --git a/pkg/util/exec.go b/pkg/util/exec.go index 87a9a3bfc..48604414b 100644 --- a/pkg/util/exec.go +++ b/pkg/util/exec.go @@ -14,8 +14,8 @@ func Exec(env, cmdName string, args ...string) (stdout bytes.Buffer, err error) fmt.Printf("[LOCAL]: Running: %s %s\n", cmdName, strings.Join(args, " ")) } - // Instantiate command object - cmd := exec.Command(cmdName, 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 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/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/rand.go b/pkg/util/rand.go deleted file mode 100644 index 7de5f3b1d..000000000 --- a/pkg/util/rand.go +++ /dev/null @@ -1,22 +0,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/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/ssh.go b/pkg/util/ssh.go index a8b89a7e8..8a9c533dc 100644 --- a/pkg/util/ssh.go +++ b/pkg/util/ssh.go @@ -51,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)) @@ -143,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 } @@ -307,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/yaml.go b/pkg/util/yaml.go index 8942d4184..e3c7274b7 100644 --- a/pkg/util/yaml.go +++ b/pkg/util/yaml.go @@ -8,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 } @@ -33,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 From e9ef28aa29fae7aa85db1faac27b0f095ef62519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 15:23:14 +0300 Subject: [PATCH 48/63] Persist controller trust CA on connect and configure. Add --ca-b64 support, NormalizeTrustCA, and SetTrustCA so trust material from flags or YAML is stored in the namespace config instead of connect-only overrides. --- internal/cmd/configure.go | 17 ++-- internal/cmd/connect.go | 13 +-- internal/configure/controlplane.go | 55 ++++++++++- internal/configure/controlplane_test.go | 97 +++++++++++++++++++ internal/configure/factory.go | 2 + internal/connect/controlplane/connect.go | 23 ++++- internal/connect/controlplane/k8s/k8s.go | 18 ++-- .../connect/controlplane/remote/remote.go | 17 ++-- internal/connect/execute.go | 17 ++-- .../testdata/remote/controlplane.yaml | 5 +- internal/resource/trust.go | 19 ++++ internal/resource/trust_test.go | 24 +++++ internal/trust/ca.go | 41 ++++++++ internal/trust/ca_test.go | 82 ++++++++++++++++ internal/trust/tls.go | 41 ++++++-- internal/trust/wait.go | 4 +- internal/util/client/client.go | 34 ++++--- .../util/client/client_local_cp_sync_test.go | 2 +- internal/util/client/client_sync_test.go | 4 +- internal/validate/password.go | 2 + 20 files changed, 450 insertions(+), 67 deletions(-) create mode 100644 internal/configure/controlplane_test.go create mode 100644 internal/resource/trust_test.go create mode 100644 internal/trust/ca.go create mode 100644 internal/trust/ca_test.go diff --git a/internal/cmd/configure.go b/internal/cmd/configure.go index 8c887f075..16e59a30e 100644 --- a/internal/cmd/configure.go +++ b/internal/cmd/configure.go @@ -14,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 { @@ -61,7 +62,9 @@ iofogctl configure controlplane --kube FILE`, 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 f1821e4bc..05b039f7d 100644 --- a/internal/cmd/connect.go +++ b/internal/cmd/connect.go @@ -14,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") @@ -48,7 +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 (connect-time override only)") + 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/configure/controlplane.go b/internal/configure/controlplane.go index e970ed84d..7b8eda133 100644 --- a/internal/configure/controlplane.go +++ b/internal/configure/controlplane.go @@ -3,6 +3,7 @@ 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" ) @@ -10,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 } @@ -29,6 +36,10 @@ func newControlPlaneExecutor(opt *Options) *controlPlaneExecutor { kubernetesConfig: kubernetesConfig{ kubeConfig: opt.KubeConfig, }, + caConfig: caConfig{ + caFile: opt.CAFile, + caB64: opt.CAB64, + }, } } @@ -37,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 @@ -49,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) @@ -65,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/factory.go b/internal/configure/factory.go index bb19ea53e..53b5b169e 100644 --- a/internal/configure/factory.go +++ b/internal/configure/factory.go @@ -14,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/connect/controlplane/connect.go b/internal/connect/controlplane/connect.go index 6f8ca1167..cfd8a1222 100644 --- a/internal/connect/controlplane/connect.go +++ b/internal/connect/controlplane/connect.go @@ -5,13 +5,32 @@ import ( "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, namespace, caFile string, ns *rsc.Namespace) error { +// 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() - agents, err := auth.ConnectLogin(context.Background(), namespace, endpoint, caFile, user.Email, user.GetRawPassword()) + agents, err := auth.ConnectLogin(context.Background(), namespace, endpoint, "", user.Email, user.GetRawPassword()) if err != nil { return err } diff --git a/internal/connect/controlplane/k8s/k8s.go b/internal/connect/controlplane/k8s/k8s.go index 1ea8da239..67bcb8b46 100644 --- a/internal/connect/controlplane/k8s/k8s.go +++ b/internal/connect/controlplane/k8s/k8s.go @@ -14,13 +14,15 @@ type kubernetesExecutor struct { controlPlane *rsc.KubernetesControlPlane namespace string caFile string + caB64 string } -func newKubernetesExecutor(controlPlane *rsc.KubernetesControlPlane, namespace, caFile string) *kubernetesExecutor { +func newKubernetesExecutor(controlPlane *rsc.KubernetesControlPlane, namespace, caFile, caB64 string) *kubernetesExecutor { return &kubernetesExecutor{ controlPlane: controlPlane, namespace: namespace, caFile: caFile, + caB64: caB64, } } @@ -28,7 +30,7 @@ func (exe *kubernetesExecutor) GetName() string { return "Kubernetes Control Plane" } -func NewManualExecutor(namespace, endpoint, kubeConfig, email, password, caFile 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, @@ -37,10 +39,10 @@ func NewManualExecutor(namespace, endpoint, kubeConfig, email, password, caFile if err := controlPlane.Sanitize(); err != nil { return nil, err } - return newKubernetesExecutor(controlPlane, namespace, caFile), nil + return newKubernetesExecutor(controlPlane, namespace, caFile, caB64), nil } -func NewExecutor(namespace, name string, yaml []byte, kind config.Kind, caFile string) (execute.Executor, error) { +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 @@ -50,7 +52,7 @@ func NewExecutor(namespace, name string, yaml []byte, kind config.Kind, caFile s return nil, err } - return newKubernetesExecutor(&controlPlane, namespace, caFile), nil + return newKubernetesExecutor(&controlPlane, namespace, caFile, caB64), nil } func (exe *kubernetesExecutor) Execute() (err error) { @@ -76,7 +78,11 @@ func (exe *kubernetesExecutor) Execute() (err error) { if err != nil { return } - err = connectcontrolplane.Connect(exe.controlPlane, endpoint, exe.namespace, exe.caFile, 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 } diff --git a/internal/connect/controlplane/remote/remote.go b/internal/connect/controlplane/remote/remote.go index 74b06d71d..d51e5ac27 100644 --- a/internal/connect/controlplane/remote/remote.go +++ b/internal/connect/controlplane/remote/remote.go @@ -16,9 +16,10 @@ type remoteExecutor struct { controlPlane *rsc.RemoteControlPlane namespace string caFile string + caB64 string } -func NewManualExecutor(namespace, name, endpoint, email, password, caFile 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 @@ -39,10 +40,10 @@ func NewManualExecutor(namespace, name, endpoint, email, password, caFile string }, } - return newRemoteExecutor(controlPlane, namespace, caFile), nil + return newRemoteExecutor(controlPlane, namespace, caFile, caB64), nil } -func NewExecutor(namespace, name string, yaml []byte, kind config.Kind, caFile string) (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 { @@ -75,14 +76,15 @@ func NewExecutor(namespace, name string, yaml []byte, kind config.Kind, caFile s } } - return newRemoteExecutor(&controlPlane, namespace, caFile), nil + return newRemoteExecutor(&controlPlane, namespace, caFile, caB64), nil } -func newRemoteExecutor(controlPlane *rsc.RemoteControlPlane, namespace, caFile string) *remoteExecutor { +func newRemoteExecutor(controlPlane *rsc.RemoteControlPlane, namespace, caFile, caB64 string) *remoteExecutor { r := &remoteExecutor{ controlPlane: controlPlane, namespace: namespace, caFile: caFile, + caB64: caB64, } return r } @@ -105,7 +107,10 @@ func (exe *remoteExecutor) Execute() (err error) { if err != nil { return err } - err = connectcontrolplane.Connect(exe.controlPlane, endpoint, exe.namespace, exe.caFile, 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 56a837bff..4fa71890a 100644 --- a/internal/connect/execute.go +++ b/internal/connect/execute.go @@ -24,6 +24,7 @@ type Options struct { Generate bool Base64Encoded bool CAFile string + CAB64 string } var kindOrder = []config.Kind{ @@ -31,13 +32,13 @@ var kindOrder = []config.Kind{ config.RemoteControlPlaneKind, } -func buildKindHandlers(caFile string) map[config.Kind]func(*execute.KindHandlerOpt) (execute.Executor, error) { +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) + 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) + return connectremotecontrolplane.NewExecutor(opt.Namespace, opt.Name, opt.YAML, config.RemoteControlPlaneKind, caFile, caB64) }, } } @@ -81,7 +82,7 @@ func Execute(opt *Options) error { defer config.Flush() if opt.InputFile != "" { - return executeWithYAML(opt.InputFile, opt.Namespace, opt.CAFile) + return executeWithYAML(opt.InputFile, opt.Namespace, opt.CAFile, opt.CAB64) } return manualExecute(opt) } @@ -94,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, opt.CAFile) + 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, opt.CAFile) + exe, err = connectremotecontrolplane.NewManualExecutor(opt.Namespace, opt.ControllerName, opt.ControllerEndpoint, opt.IofogUserEmail, opt.IofogUserPass, opt.CAFile, opt.CAB64) if err != nil { return err } @@ -112,8 +113,8 @@ func manualExecute(opt *Options) (err error) { return nil } -func executeWithYAML(yamlFile, namespace, caFile string) error { - handlers := buildKindHandlers(caFile) +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/resource/testdata/remote/controlplane.yaml b/internal/resource/testdata/remote/controlplane.yaml index 83b340a1f..dbbf03054 100644 --- a/internal/resource/testdata/remote/controlplane.yaml +++ b/internal/resource/testdata/remote/controlplane.yaml @@ -4,6 +4,7 @@ metadata: name: iofog spec: # endpoint: https://controller.example.com + ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlETGpDQ0FoYWdBd0lCQWdJV0NBQmhPQkJBZFloM01tbDBGaUlnUlhVVEZvWU9OekFOQmdrcWhraUc5dzBCDQpBUXNGQURBUU1RNHdEQVlEVlFRREV3VnBiMlp2WnpBZUZ3MHlOakEyTWpRd09UTTVNREJhRncweU9UQTJNalF3DQpOVFE0TXpaYU1CQXhEakFNQmdOVkJBTVRCV2x2Wm05bk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBDQpNSUlCQ2dLQ0FRRUFxYlRjZFJmdU1iYmNHQUpNK0NDRk5JU1lKNDhpMENhUURRMWhDRmFtc2I3K0pmMElScml0DQpaTCtsb2d2a0Q4RW5SUWNhS0RmMkkvNDljNkF1YiszSERkMTJCa2N1NGRUNEQ0blduazVJNHJXakxRMGdzSGRaDQpNS3YwMVpUUjE4SG5JNTJaOWtLYnBtaFJiei9SSitWeGpqb2llWDVaTkplZVNwL2JIYk9TeUFROFhFSlA1MEZ4DQpQVHo3SktwUFN3bHE2dDBhWDVFUy9XajNuNXNhQTYybzBpV01vbWFVaTRGeG95RUljekJsbWd4RE9BdEM2UjV6DQpFbkcyeExJcklFWVBCUmJBWlIyNnU5N0lGTWI0eUhnYWJJSkNUM3JWUDYzYkdabmNlM0lhaUUrSzRoZDN5K2wwDQp5RVRPdzMxY2pHa0wwSTEyVzlKY1E2MDNrcStRUVUrK3dRSURBUUFCbzM0d2ZEQU1CZ05WSFJNQkFmOEVBakFBDQpNQTRHQTFVZER3RUIvd1FFQXdJRm9EQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3DQpIZ1lEVlIwUkJCY3dGWWNFd0tocEFvSU5NVGt5TGpFMk9DNHhNRFV1TWpBZEJnTlZIUTRFRmdRVWZTWWJQZnJQDQprZ3NabmlFaUdBeFJGUGdZYlBnd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFZnkzeSt0ZGtnTXVxa3FQZDdpDQoxNWk5elQxeC9ybm5uVjIvRDYxVy9rLzVsZWhSMERRNkxqbWxoNERaMi81b1YxY3JwVHZWUkw5R0dCRUtNdUM1DQo4dzk5NWY3WEQ2eWVabHAwdkJhSlo0OXVHQ1FGMGc5K09kSkNIeHVETU1zaWcxRDk4ZnJYV2tjZkZ4OW9VcW1SDQptQi80VkVCdkt6SVlVREJwZ0lQeWozV24zR1g5WVFtM3J1TjlmNVBvTUsyeHpXVVE3SW5OMjF6WTV4Q2dVajBhDQo1OEhjc002SWdXbW4xU0FaY0h2bGZpaGgvbmtIY3ZDVXA0VzJEeFRNQXFxM2xNSWIwTS9JOWxaYXZaMi8ySUlZDQpFbE1kSkRCdjd6OFUveElnbFZvaWlSM3p6Yjd4T25ZWnRiS0dNQlVIYThoeVI1ekZaWGRrQ25rRFpPc2gxZzh1DQpReDg9DQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tDQo= iofogUser: name: Foo surname: Bar @@ -17,7 +18,7 @@ spec: image: ghcr.io/datasance/controller:3.8.0-rc.4 auth: mode: embedded - insecureAllowHttp: true + insecureAllowHttp: false insecureAllowBootstrapLog: false bootstrap: username: admin @@ -63,7 +64,7 @@ spec: ssh: user: emirhan keyFile: /Users/emirhan/.lima/_config/user - port: 57884 + port: 61954 systemAgent: config: host: 192.168.105.2 diff --git a/internal/resource/trust.go b/internal/resource/trust.go index 61c61ed64..a9e443fbf 100644 --- a/internal/resource/trust.go +++ b/internal/resource/trust.go @@ -1,5 +1,9 @@ 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 { @@ -13,3 +17,18 @@ func GetTrustCA(cp ControlPlane) string { } 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/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/tls.go b/internal/trust/tls.go index ef163e523..f41acba44 100644 --- a/internal/trust/tls.go +++ b/internal/trust/tls.go @@ -6,9 +6,9 @@ import ( "crypto/x509" "fmt" "net/url" - "os" "strings" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -34,14 +34,14 @@ func TransportFromPEM(pem []byte) (TransportConfig, error) { // TransportFromCAFile loads a PEM file for connect-time CA override. func TransportFromCAFile(caFile string) (TransportConfig, error) { - pem, err := os.ReadFile(caFile) + 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; caFile overrides the namespace store. +// 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) @@ -93,9 +93,8 @@ func ResolveTransport(ctx context.Context, namespace, endpoint string) Transport TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}, } case IsUnknownAuthority(probeErr): - util.PrintWarning(fmt.Sprintf( + util.PrintWarning(insecureTLSWarning(namespace, `Controller TLS certificate is not trusted by the system CA store for namespace "%s". Certificate verification is disabled.`, - namespace, )) _ = SetCachedMode(namespace, ModeInsecure) return TransportConfig{ @@ -112,6 +111,9 @@ func ResolveTransport(ctx context.Context, namespace, endpoint string) Transport 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 @@ -119,6 +121,32 @@ func ResolveTransport(ctx context.Context, namespace, endpoint string) Transport } } +// 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() @@ -129,8 +157,5 @@ func hostPort(u *url.URL) (host, port string) { port = "80" } } - if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") { - // bare IPv6 without brackets handled by Hostname() - } return host, port } diff --git a/internal/trust/wait.go b/internal/trust/wait.go index b1e2351df..763b7869c 100644 --- a/internal/trust/wait.go +++ b/internal/trust/wait.go @@ -3,7 +3,6 @@ package trust import ( "context" "fmt" - "io" "net/http" "time" @@ -35,8 +34,7 @@ func WaitForControllerAPI(ctx context.Context, namespace, endpoint string) error } resp, err := client.Do(req) if err == nil { - _, _ = io.Copy(io.Discard, resp.Body) - resp.Body.Close() + util.DrainAndCloseHTTPBody(resp.Body) if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil } diff --git a/internal/util/client/client.go b/internal/util/client/client.go index f8b314cdc..0755a039e 100644 --- a/internal/util/client/client.go +++ b/internal/util/client/client.go @@ -1,16 +1,23 @@ 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 { @@ -283,12 +290,6 @@ 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() @@ -296,9 +297,14 @@ func newControllerClient(namespace string) (*client.Client, error) { // controlPlane.UpdateUserTokens(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: %w", err) @@ -306,7 +312,9 @@ func newControllerClient(namespace string) (*client.Client, error) { 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 } @@ -325,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 } @@ -457,14 +466,15 @@ 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: %w", err) diff --git a/internal/util/client/client_local_cp_sync_test.go b/internal/util/client/client_local_cp_sync_test.go index 3fdfbabcf..196667bfd 100644 --- a/internal/util/client/client_local_cp_sync_test.go +++ b/internal/util/client/client_local_cp_sync_test.go @@ -47,7 +47,7 @@ func TestMergeRemoteAgentFromBackendPreservesSSHHostForLocalCPAgents(t *testing. Host: sshHost, SSH: rsc.SSH{User: "ubuntu", Port: 22, KeyFile: "/tmp/id_ed25519"}, Package: rsc.Package{ - Version: "v1.0.0-rc.4", + Version: "v1.0.0-rc.5", }, } diff --git a/internal/util/client/client_sync_test.go b/internal/util/client/client_sync_test.go index 1396dda94..a7bf83f90 100644 --- a/internal/util/client/client_sync_test.go +++ b/internal/util/client/client_sync_test.go @@ -18,7 +18,7 @@ func TestMergeRemoteAgentFromBackendPreservesSSHHost(t *testing.T) { SSH: rsc.SSH{User: "ubuntu2", Port: 32222, KeyFile: "/tmp/id_ed25519"}, Airgap: true, Package: rsc.Package{ - Version: "v1.0.0-rc.4", + Version: "v1.0.0-rc.5", }, } @@ -34,7 +34,7 @@ func TestMergeRemoteAgentFromBackendPreservesSSHHost(t *testing.T) { 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.4" { + if merged.SSH.User != "ubuntu2" || !merged.Airgap || merged.Package.Version != "v1.0.0-rc.5" { t.Fatalf("cached deploy metadata lost: %+v", merged) } if merged.UUID != "055bcc8d-91d2-445e-b7a2-f48e5bd98046" { diff --git a/internal/validate/password.go b/internal/validate/password.go index a77961dd9..3a569459a 100644 --- a/internal/validate/password.go +++ b/internal/validate/password.go @@ -9,6 +9,8 @@ import ( // 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 { From 08aecb2ce07efa8af1cc5d184eb45e9ece1acb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 15:23:21 +0300 Subject: [PATCH 49/63] Use build-time binary name in CLI help and split generated docs. Format Cobra help with the active flavor name and emit markdown into separate iofogctl_md and potctl_md directories. --- docs/{md => iofogctl_md}/iofogctl.md | 1 + docs/{md => iofogctl_md}/iofogctl_attach.md | 2 +- .../iofogctl_attach_agent.md | 0 .../iofogctl_attach_exec.md | 0 .../iofogctl_attach_exec_agent.md | 0 .../iofogctl_attach_exec_microservice.md | 0 .../iofogctl_attach_volume-mount.md | 0 .../iofogctl_completion.md | 0 .../iofogctl_completion_bash.md | 0 .../iofogctl_completion_fish.md | 0 .../iofogctl_completion_powershell.md | 0 .../iofogctl_completion_zsh.md | 0 .../{md => iofogctl_md}/iofogctl_configure.md | 15 +++-- docs/{md => iofogctl_md}/iofogctl_connect.md | 4 +- docs/{md => iofogctl_md}/iofogctl_create.md | 0 .../iofogctl_create_namespace.md | 0 docs/{md => iofogctl_md}/iofogctl_delete.md | 5 +- .../iofogctl_delete_agent.md | 1 + .../iofogctl_delete_all.md | 1 + .../iofogctl_delete_application-template.md | 1 + .../iofogctl_delete_application.md | 1 + .../iofogctl_delete_catalogitem.md | 1 + .../iofogctl_delete_certificate.md | 1 + .../iofogctl_delete_configmap.md | 1 + .../iofogctl_delete_controller.md | 1 + .../iofogctl_delete_microservice.md | 1 + .../iofogctl_delete_namespace.md | 1 + .../iofogctl_delete_nats-account-rule.md | 1 + .../iofogctl_delete_nats-user-rule.md | 1 + .../iofogctl_delete_registry.md | 1 + .../iofogctl_delete_role.md | 1 + .../iofogctl_delete_rolebinding.md | 1 + .../iofogctl_delete_secret.md | 1 + .../iofogctl_delete_service.md | 1 + .../iofogctl_delete_serviceaccount.md | 1 + .../iofogctl_delete_volume-mount.md | 1 + .../iofogctl_delete_volume.md | 1 + docs/{md => iofogctl_md}/iofogctl_deploy.md | 4 +- docs/{md => iofogctl_md}/iofogctl_describe.md | 0 .../iofogctl_describe_agent-config.md | 0 .../iofogctl_describe_agent.md | 0 .../iofogctl_describe_application-template.md | 0 .../iofogctl_describe_application.md | 0 .../iofogctl_describe_certificate.md | 0 .../iofogctl_describe_configmap.md | 0 .../iofogctl_describe_controller.md | 0 .../iofogctl_describe_controlplane.md | 0 .../iofogctl_describe_microservice.md | 0 .../iofogctl_describe_namespace.md | 0 .../iofogctl_describe_nats-account-rule.md | 0 .../iofogctl_describe_nats-account.md | 0 .../iofogctl_describe_nats-user-rule.md | 0 .../iofogctl_describe_nats-user.md | 0 .../iofogctl_describe_registry.md | 0 .../iofogctl_describe_role.md | 0 .../iofogctl_describe_rolebinding.md | 0 .../iofogctl_describe_secret.md | 0 .../iofogctl_describe_service.md | 0 .../iofogctl_describe_serviceaccount.md | 0 .../iofogctl_describe_system-microservice.md | 0 .../iofogctl_describe_volume-mount.md | 0 .../iofogctl_describe_volume.md | 0 docs/{md => iofogctl_md}/iofogctl_detach.md | 2 +- .../iofogctl_detach_agent.md | 0 .../iofogctl_detach_exec.md | 0 .../iofogctl_detach_exec_agent.md | 0 .../iofogctl_detach_exec_microservice.md | 0 .../iofogctl_detach_volume-mount.md | 0 .../iofogctl_disconnect.md | 0 docs/{md => iofogctl_md}/iofogctl_exec.md | 0 .../iofogctl_exec_agent.md | 0 .../iofogctl_exec_microservice.md | 0 docs/{md => iofogctl_md}/iofogctl_get.md | 0 docs/{md => iofogctl_md}/iofogctl_logs.md | 0 docs/{md => iofogctl_md}/iofogctl_move.md | 0 .../iofogctl_move_agent.md | 0 .../iofogctl_move_microservice.md | 0 docs/{md => iofogctl_md}/iofogctl_nats.md | 0 .../iofogctl_nats_accounts.md | 0 .../iofogctl_nats_accounts_ensure.md | 0 .../iofogctl_nats_operator.md | 0 .../iofogctl_nats_operator_describe.md | 0 .../iofogctl_nats_users.md | 0 .../iofogctl_nats_users_create-mqtt-bearer.md | 0 .../iofogctl_nats_users_create.md | 0 .../iofogctl_nats_users_creds.md | 0 .../iofogctl_nats_users_delete-mqtt-bearer.md | 0 .../iofogctl_nats_users_delete.md | 0 docs/{md => iofogctl_md}/iofogctl_prune.md | 0 .../iofogctl_prune_agent.md | 0 docs/{md => iofogctl_md}/iofogctl_rebuild.md | 0 .../iofogctl_rebuild_microservice.md | 0 .../iofogctl_rebuild_system-microservice.md | 0 docs/iofogctl_md/iofogctl_reconcile.md | 29 +++++++++ docs/iofogctl_md/iofogctl_reconcile_agent.md | 33 ++++++++++ .../iofogctl_md/iofogctl_reconcile_service.md | 33 ++++++++++ docs/{md => iofogctl_md}/iofogctl_rollback.md | 0 docs/{md => iofogctl_md}/iofogctl_start.md | 0 .../iofogctl_start_application.md | 0 .../iofogctl_start_microservice.md | 0 docs/{md => iofogctl_md}/iofogctl_stop.md | 0 .../iofogctl_stop_application.md | 0 .../iofogctl_stop_microservice.md | 0 docs/{md => iofogctl_md}/iofogctl_upgrade.md | 0 docs/{md => iofogctl_md}/iofogctl_version.md | 0 docs/{md => iofogctl_md}/iofogctl_view.md | 0 docs/potctl_md/potctl.md | 45 +++++++++++++ docs/potctl_md/potctl_attach.md | 36 +++++++++++ docs/potctl_md/potctl_attach_agent.md | 39 ++++++++++++ docs/potctl_md/potctl_attach_exec.md | 35 +++++++++++ docs/potctl_md/potctl_attach_exec_agent.md | 37 +++++++++++ .../potctl_attach_exec_microservice.md | 37 +++++++++++ docs/potctl_md/potctl_attach_volume-mount.md | 37 +++++++++++ docs/potctl_md/potctl_completion.md | 33 ++++++++++ docs/potctl_md/potctl_completion_bash.md | 52 +++++++++++++++ docs/potctl_md/potctl_completion_fish.md | 43 +++++++++++++ .../potctl_md/potctl_completion_powershell.md | 40 ++++++++++++ docs/potctl_md/potctl_completion_zsh.md | 54 ++++++++++++++++ docs/potctl_md/potctl_configure.md | 54 ++++++++++++++++ docs/potctl_md/potctl_connect.md | 57 +++++++++++++++++ docs/potctl_md/potctl_create.md | 28 +++++++++ docs/potctl_md/potctl_create_namespace.md | 41 ++++++++++++ docs/potctl_md/potctl_delete.md | 53 ++++++++++++++++ docs/potctl_md/potctl_delete_agent.md | 46 ++++++++++++++ docs/potctl_md/potctl_delete_all.md | 44 +++++++++++++ .../potctl_delete_application-template.md | 38 +++++++++++ docs/potctl_md/potctl_delete_application.md | 38 +++++++++++ docs/potctl_md/potctl_delete_catalogitem.md | 38 +++++++++++ docs/potctl_md/potctl_delete_certificate.md | 38 +++++++++++ docs/potctl_md/potctl_delete_configmap.md | 38 +++++++++++ docs/potctl_md/potctl_delete_controller.md | 38 +++++++++++ docs/potctl_md/potctl_delete_microservice.md | 38 +++++++++++ docs/potctl_md/potctl_delete_namespace.md | 43 +++++++++++++ .../potctl_delete_nats-account-rule.md | 38 +++++++++++ .../potctl_md/potctl_delete_nats-user-rule.md | 38 +++++++++++ docs/potctl_md/potctl_delete_registry.md | 38 +++++++++++ docs/potctl_md/potctl_delete_role.md | 38 +++++++++++ docs/potctl_md/potctl_delete_rolebinding.md | 38 +++++++++++ docs/potctl_md/potctl_delete_secret.md | 38 +++++++++++ docs/potctl_md/potctl_delete_service.md | 38 +++++++++++ .../potctl_md/potctl_delete_serviceaccount.md | 38 +++++++++++ docs/potctl_md/potctl_delete_volume-mount.md | 38 +++++++++++ docs/potctl_md/potctl_delete_volume.md | 40 ++++++++++++ docs/potctl_md/potctl_deploy.md | 51 +++++++++++++++ docs/potctl_md/potctl_describe.md | 53 ++++++++++++++++ .../potctl_md/potctl_describe_agent-config.md | 38 +++++++++++ docs/potctl_md/potctl_describe_agent.md | 39 ++++++++++++ .../potctl_describe_application-template.md | 38 +++++++++++ docs/potctl_md/potctl_describe_application.md | 38 +++++++++++ docs/potctl_md/potctl_describe_certificate.md | 38 +++++++++++ docs/potctl_md/potctl_describe_configmap.md | 38 +++++++++++ docs/potctl_md/potctl_describe_controller.md | 38 +++++++++++ .../potctl_md/potctl_describe_controlplane.md | 38 +++++++++++ .../potctl_md/potctl_describe_microservice.md | 38 +++++++++++ docs/potctl_md/potctl_describe_namespace.md | 38 +++++++++++ .../potctl_describe_nats-account-rule.md | 32 ++++++++++ .../potctl_md/potctl_describe_nats-account.md | 33 ++++++++++ .../potctl_describe_nats-user-rule.md | 32 ++++++++++ docs/potctl_md/potctl_describe_nats-user.md | 33 ++++++++++ docs/potctl_md/potctl_describe_registry.md | 38 +++++++++++ docs/potctl_md/potctl_describe_role.md | 38 +++++++++++ docs/potctl_md/potctl_describe_rolebinding.md | 38 +++++++++++ docs/potctl_md/potctl_describe_secret.md | 38 +++++++++++ docs/potctl_md/potctl_describe_service.md | 38 +++++++++++ .../potctl_describe_serviceaccount.md | 38 +++++++++++ .../potctl_describe_system-microservice.md | 38 +++++++++++ .../potctl_md/potctl_describe_volume-mount.md | 38 +++++++++++ docs/potctl_md/potctl_describe_volume.md | 38 +++++++++++ docs/potctl_md/potctl_detach.md | 36 +++++++++++ docs/potctl_md/potctl_detach_agent.md | 45 +++++++++++++ docs/potctl_md/potctl_detach_exec.md | 35 +++++++++++ docs/potctl_md/potctl_detach_exec_agent.md | 37 +++++++++++ .../potctl_detach_exec_microservice.md | 37 +++++++++++ docs/potctl_md/potctl_detach_volume-mount.md | 37 +++++++++++ docs/potctl_md/potctl_disconnect.md | 41 ++++++++++++ docs/potctl_md/potctl_exec.md | 29 +++++++++ docs/potctl_md/potctl_exec_agent.md | 37 +++++++++++ docs/potctl_md/potctl_exec_microservice.md | 37 +++++++++++ docs/potctl_md/potctl_get.md | 63 +++++++++++++++++++ docs/potctl_md/potctl_logs.md | 43 +++++++++++++ docs/potctl_md/potctl_move.md | 29 +++++++++ docs/potctl_md/potctl_move_agent.md | 38 +++++++++++ docs/potctl_md/potctl_move_microservice.md | 37 +++++++++++ docs/potctl_md/potctl_nats.md | 39 ++++++++++++ docs/potctl_md/potctl_nats_accounts.md | 34 ++++++++++ docs/potctl_md/potctl_nats_accounts_ensure.md | 29 +++++++++ docs/potctl_md/potctl_nats_operator.md | 28 +++++++++ .../potctl_nats_operator_describe.md | 29 +++++++++ docs/potctl_md/potctl_nats_users.md | 40 ++++++++++++ .../potctl_nats_users_create-mqtt-bearer.md | 30 +++++++++ docs/potctl_md/potctl_nats_users_create.md | 30 +++++++++ docs/potctl_md/potctl_nats_users_creds.md | 28 +++++++++ .../potctl_nats_users_delete-mqtt-bearer.md | 27 ++++++++ docs/potctl_md/potctl_nats_users_delete.md | 27 ++++++++ docs/potctl_md/potctl_prune.md | 28 +++++++++ docs/potctl_md/potctl_prune_agent.md | 38 +++++++++++ docs/potctl_md/potctl_rebuild.md | 29 +++++++++ docs/potctl_md/potctl_rebuild_microservice.md | 37 +++++++++++ .../potctl_rebuild_system-microservice.md | 37 +++++++++++ docs/potctl_md/potctl_reconcile.md | 29 +++++++++ docs/potctl_md/potctl_reconcile_agent.md | 33 ++++++++++ docs/potctl_md/potctl_reconcile_service.md | 33 ++++++++++ docs/potctl_md/potctl_rollback.md | 37 +++++++++++ docs/potctl_md/potctl_start.md | 29 +++++++++ docs/potctl_md/potctl_start_application.md | 37 +++++++++++ docs/potctl_md/potctl_start_microservice.md | 37 +++++++++++ docs/potctl_md/potctl_stop.md | 29 +++++++++ docs/potctl_md/potctl_stop_application.md | 37 +++++++++++ docs/potctl_md/potctl_stop_microservice.md | 37 +++++++++++ docs/potctl_md/potctl_upgrade.md | 37 +++++++++++ docs/potctl_md/potctl_version.md | 28 +++++++++ docs/potctl_md/potctl_view.md | 27 ++++++++ gitHooks/pre-commit | 11 ++-- internal/cmd/attach.go | 2 +- internal/cmd/attach_agent.go | 2 +- internal/cmd/attach_exec.go | 6 +- internal/cmd/attach_volume_moount.go | 2 +- internal/cmd/bash_complete.go | 6 +- internal/cmd/create_namespace.go | 6 +- internal/cmd/delete_agent.go | 6 +- internal/cmd/delete_all.go | 2 +- internal/cmd/delete_application.go | 2 +- internal/cmd/delete_catalog_item.go | 2 +- internal/cmd/delete_certificate.go | 2 +- internal/cmd/delete_config_map.go | 2 +- internal/cmd/delete_controller.go | 2 +- internal/cmd/delete_microservice.go | 2 +- internal/cmd/delete_namespace.go | 2 +- internal/cmd/delete_nats_account_rule.go | 2 +- internal/cmd/delete_nats_user_rule.go | 2 +- internal/cmd/delete_registry.go | 2 +- internal/cmd/delete_role.go | 2 +- internal/cmd/delete_rolebinding.go | 2 +- internal/cmd/delete_secret.go | 2 +- internal/cmd/delete_service.go | 2 +- internal/cmd/delete_serviceaccount.go | 2 +- internal/cmd/delete_template.go | 2 +- internal/cmd/delete_volume.go | 2 +- internal/cmd/delete_volume_mount.go | 2 +- internal/cmd/deploy.go | 8 +-- internal/cmd/describe_agent.go | 2 +- internal/cmd/describe_agent_config.go | 2 +- internal/cmd/describe_application.go | 2 +- internal/cmd/describe_certificate.go | 2 +- internal/cmd/describe_config_map.go | 2 +- internal/cmd/describe_controller.go | 2 +- internal/cmd/describe_controlplane.go | 2 +- internal/cmd/describe_microservice.go | 2 +- internal/cmd/describe_namespace.go | 2 +- internal/cmd/describe_registry.go | 2 +- internal/cmd/describe_role.go | 2 +- internal/cmd/describe_rolebinding.go | 2 +- internal/cmd/describe_secret.go | 2 +- internal/cmd/describe_service.go | 2 +- internal/cmd/describe_serviceaccount.go | 2 +- internal/cmd/describe_system_microservice.go | 2 +- internal/cmd/describe_template.go | 2 +- internal/cmd/describe_volume.go | 2 +- internal/cmd/describe_volume_mount.go | 2 +- internal/cmd/detach.go | 2 +- internal/cmd/detach_agent.go | 2 +- internal/cmd/detach_exec.go | 6 +- internal/cmd/detach_volume_mount.go | 2 +- internal/cmd/disconnect.go | 2 +- internal/cmd/exec_agent.go | 2 +- internal/cmd/exec_microservice.go | 2 +- internal/cmd/generate_documentation.go | 21 +++---- internal/cmd/get.go | 4 +- internal/cmd/help.go | 13 ++++ internal/cmd/logs.go | 4 +- internal/cmd/move_agent.go | 2 +- internal/cmd/move_microservice.go | 2 +- internal/cmd/nats.go | 16 ++--- internal/cmd/prune_agent.go | 2 +- internal/cmd/rebuild_microservice.go | 2 +- internal/cmd/rebuild_system_microservice.go | 2 +- internal/cmd/reconcile.go | 4 +- internal/cmd/rollback.go | 2 +- internal/cmd/start_application.go | 2 +- internal/cmd/start_microservice.go | 2 +- internal/cmd/stop_application.go | 2 +- internal/cmd/stop_microservice.go | 2 +- internal/cmd/upgrade.go | 2 +- 283 files changed, 4235 insertions(+), 120 deletions(-) rename docs/{md => iofogctl_md}/iofogctl.md (95%) rename docs/{md => iofogctl_md}/iofogctl_attach.md (98%) rename docs/{md => iofogctl_md}/iofogctl_attach_agent.md (100%) rename docs/{md => iofogctl_md}/iofogctl_attach_exec.md (100%) rename docs/{md => iofogctl_md}/iofogctl_attach_exec_agent.md (100%) rename docs/{md => iofogctl_md}/iofogctl_attach_exec_microservice.md (100%) rename docs/{md => iofogctl_md}/iofogctl_attach_volume-mount.md (100%) rename docs/{md => iofogctl_md}/iofogctl_completion.md (100%) rename docs/{md => iofogctl_md}/iofogctl_completion_bash.md (100%) rename docs/{md => iofogctl_md}/iofogctl_completion_fish.md (100%) rename docs/{md => iofogctl_md}/iofogctl_completion_powershell.md (100%) rename docs/{md => iofogctl_md}/iofogctl_completion_zsh.md (100%) rename docs/{md => iofogctl_md}/iofogctl_configure.md (61%) rename docs/{md => iofogctl_md}/iofogctl_connect.md (84%) rename docs/{md => iofogctl_md}/iofogctl_create.md (100%) rename docs/{md => iofogctl_md}/iofogctl_create_namespace.md (100%) rename docs/{md => iofogctl_md}/iofogctl_delete.md (90%) rename docs/{md => iofogctl_md}/iofogctl_delete_agent.md (91%) rename docs/{md => iofogctl_md}/iofogctl_delete_all.md (91%) rename docs/{md => iofogctl_md}/iofogctl_delete_application-template.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_application.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_catalogitem.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_certificate.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_configmap.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_controller.md (87%) rename docs/{md => iofogctl_md}/iofogctl_delete_microservice.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_namespace.md (90%) rename docs/{md => iofogctl_md}/iofogctl_delete_nats-account-rule.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_nats-user-rule.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_registry.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_role.md (87%) rename docs/{md => iofogctl_md}/iofogctl_delete_rolebinding.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_secret.md (87%) rename docs/{md => iofogctl_md}/iofogctl_delete_service.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_serviceaccount.md (90%) rename docs/{md => iofogctl_md}/iofogctl_delete_volume-mount.md (88%) rename docs/{md => iofogctl_md}/iofogctl_delete_volume.md (88%) rename docs/{md => iofogctl_md}/iofogctl_deploy.md (91%) rename docs/{md => iofogctl_md}/iofogctl_describe.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_agent-config.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_agent.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_application-template.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_application.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_certificate.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_configmap.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_controller.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_controlplane.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_microservice.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_namespace.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_nats-account-rule.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_nats-account.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_nats-user-rule.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_nats-user.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_registry.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_role.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_rolebinding.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_secret.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_service.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_serviceaccount.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_system-microservice.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_volume-mount.md (100%) rename docs/{md => iofogctl_md}/iofogctl_describe_volume.md (100%) rename docs/{md => iofogctl_md}/iofogctl_detach.md (98%) rename docs/{md => iofogctl_md}/iofogctl_detach_agent.md (100%) rename docs/{md => iofogctl_md}/iofogctl_detach_exec.md (100%) rename docs/{md => iofogctl_md}/iofogctl_detach_exec_agent.md (100%) rename docs/{md => iofogctl_md}/iofogctl_detach_exec_microservice.md (100%) rename docs/{md => iofogctl_md}/iofogctl_detach_volume-mount.md (100%) rename docs/{md => iofogctl_md}/iofogctl_disconnect.md (100%) rename docs/{md => iofogctl_md}/iofogctl_exec.md (100%) rename docs/{md => iofogctl_md}/iofogctl_exec_agent.md (100%) rename docs/{md => iofogctl_md}/iofogctl_exec_microservice.md (100%) rename docs/{md => iofogctl_md}/iofogctl_get.md (100%) rename docs/{md => iofogctl_md}/iofogctl_logs.md (100%) rename docs/{md => iofogctl_md}/iofogctl_move.md (100%) rename docs/{md => iofogctl_md}/iofogctl_move_agent.md (100%) rename docs/{md => iofogctl_md}/iofogctl_move_microservice.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats_accounts.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats_accounts_ensure.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats_operator.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats_operator_describe.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats_users.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats_users_create-mqtt-bearer.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats_users_create.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats_users_creds.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats_users_delete-mqtt-bearer.md (100%) rename docs/{md => iofogctl_md}/iofogctl_nats_users_delete.md (100%) rename docs/{md => iofogctl_md}/iofogctl_prune.md (100%) rename docs/{md => iofogctl_md}/iofogctl_prune_agent.md (100%) rename docs/{md => iofogctl_md}/iofogctl_rebuild.md (100%) rename docs/{md => iofogctl_md}/iofogctl_rebuild_microservice.md (100%) rename docs/{md => iofogctl_md}/iofogctl_rebuild_system-microservice.md (100%) create mode 100644 docs/iofogctl_md/iofogctl_reconcile.md create mode 100644 docs/iofogctl_md/iofogctl_reconcile_agent.md create mode 100644 docs/iofogctl_md/iofogctl_reconcile_service.md rename docs/{md => iofogctl_md}/iofogctl_rollback.md (100%) rename docs/{md => iofogctl_md}/iofogctl_start.md (100%) rename docs/{md => iofogctl_md}/iofogctl_start_application.md (100%) rename docs/{md => iofogctl_md}/iofogctl_start_microservice.md (100%) rename docs/{md => iofogctl_md}/iofogctl_stop.md (100%) rename docs/{md => iofogctl_md}/iofogctl_stop_application.md (100%) rename docs/{md => iofogctl_md}/iofogctl_stop_microservice.md (100%) rename docs/{md => iofogctl_md}/iofogctl_upgrade.md (100%) rename docs/{md => iofogctl_md}/iofogctl_version.md (100%) rename docs/{md => iofogctl_md}/iofogctl_view.md (100%) create mode 100644 docs/potctl_md/potctl.md create mode 100644 docs/potctl_md/potctl_attach.md create mode 100644 docs/potctl_md/potctl_attach_agent.md create mode 100644 docs/potctl_md/potctl_attach_exec.md create mode 100644 docs/potctl_md/potctl_attach_exec_agent.md create mode 100644 docs/potctl_md/potctl_attach_exec_microservice.md create mode 100644 docs/potctl_md/potctl_attach_volume-mount.md create mode 100644 docs/potctl_md/potctl_completion.md create mode 100644 docs/potctl_md/potctl_completion_bash.md create mode 100644 docs/potctl_md/potctl_completion_fish.md create mode 100644 docs/potctl_md/potctl_completion_powershell.md create mode 100644 docs/potctl_md/potctl_completion_zsh.md create mode 100644 docs/potctl_md/potctl_configure.md create mode 100644 docs/potctl_md/potctl_connect.md create mode 100644 docs/potctl_md/potctl_create.md create mode 100644 docs/potctl_md/potctl_create_namespace.md create mode 100644 docs/potctl_md/potctl_delete.md create mode 100644 docs/potctl_md/potctl_delete_agent.md create mode 100644 docs/potctl_md/potctl_delete_all.md create mode 100644 docs/potctl_md/potctl_delete_application-template.md create mode 100644 docs/potctl_md/potctl_delete_application.md create mode 100644 docs/potctl_md/potctl_delete_catalogitem.md create mode 100644 docs/potctl_md/potctl_delete_certificate.md create mode 100644 docs/potctl_md/potctl_delete_configmap.md create mode 100644 docs/potctl_md/potctl_delete_controller.md create mode 100644 docs/potctl_md/potctl_delete_microservice.md create mode 100644 docs/potctl_md/potctl_delete_namespace.md create mode 100644 docs/potctl_md/potctl_delete_nats-account-rule.md create mode 100644 docs/potctl_md/potctl_delete_nats-user-rule.md create mode 100644 docs/potctl_md/potctl_delete_registry.md create mode 100644 docs/potctl_md/potctl_delete_role.md create mode 100644 docs/potctl_md/potctl_delete_rolebinding.md create mode 100644 docs/potctl_md/potctl_delete_secret.md create mode 100644 docs/potctl_md/potctl_delete_service.md create mode 100644 docs/potctl_md/potctl_delete_serviceaccount.md create mode 100644 docs/potctl_md/potctl_delete_volume-mount.md create mode 100644 docs/potctl_md/potctl_delete_volume.md create mode 100644 docs/potctl_md/potctl_deploy.md create mode 100644 docs/potctl_md/potctl_describe.md create mode 100644 docs/potctl_md/potctl_describe_agent-config.md create mode 100644 docs/potctl_md/potctl_describe_agent.md create mode 100644 docs/potctl_md/potctl_describe_application-template.md create mode 100644 docs/potctl_md/potctl_describe_application.md create mode 100644 docs/potctl_md/potctl_describe_certificate.md create mode 100644 docs/potctl_md/potctl_describe_configmap.md create mode 100644 docs/potctl_md/potctl_describe_controller.md create mode 100644 docs/potctl_md/potctl_describe_controlplane.md create mode 100644 docs/potctl_md/potctl_describe_microservice.md create mode 100644 docs/potctl_md/potctl_describe_namespace.md create mode 100644 docs/potctl_md/potctl_describe_nats-account-rule.md create mode 100644 docs/potctl_md/potctl_describe_nats-account.md create mode 100644 docs/potctl_md/potctl_describe_nats-user-rule.md create mode 100644 docs/potctl_md/potctl_describe_nats-user.md create mode 100644 docs/potctl_md/potctl_describe_registry.md create mode 100644 docs/potctl_md/potctl_describe_role.md create mode 100644 docs/potctl_md/potctl_describe_rolebinding.md create mode 100644 docs/potctl_md/potctl_describe_secret.md create mode 100644 docs/potctl_md/potctl_describe_service.md create mode 100644 docs/potctl_md/potctl_describe_serviceaccount.md create mode 100644 docs/potctl_md/potctl_describe_system-microservice.md create mode 100644 docs/potctl_md/potctl_describe_volume-mount.md create mode 100644 docs/potctl_md/potctl_describe_volume.md create mode 100644 docs/potctl_md/potctl_detach.md create mode 100644 docs/potctl_md/potctl_detach_agent.md create mode 100644 docs/potctl_md/potctl_detach_exec.md create mode 100644 docs/potctl_md/potctl_detach_exec_agent.md create mode 100644 docs/potctl_md/potctl_detach_exec_microservice.md create mode 100644 docs/potctl_md/potctl_detach_volume-mount.md create mode 100644 docs/potctl_md/potctl_disconnect.md create mode 100644 docs/potctl_md/potctl_exec.md create mode 100644 docs/potctl_md/potctl_exec_agent.md create mode 100644 docs/potctl_md/potctl_exec_microservice.md create mode 100644 docs/potctl_md/potctl_get.md create mode 100644 docs/potctl_md/potctl_logs.md create mode 100644 docs/potctl_md/potctl_move.md create mode 100644 docs/potctl_md/potctl_move_agent.md create mode 100644 docs/potctl_md/potctl_move_microservice.md create mode 100644 docs/potctl_md/potctl_nats.md create mode 100644 docs/potctl_md/potctl_nats_accounts.md create mode 100644 docs/potctl_md/potctl_nats_accounts_ensure.md create mode 100644 docs/potctl_md/potctl_nats_operator.md create mode 100644 docs/potctl_md/potctl_nats_operator_describe.md create mode 100644 docs/potctl_md/potctl_nats_users.md create mode 100644 docs/potctl_md/potctl_nats_users_create-mqtt-bearer.md create mode 100644 docs/potctl_md/potctl_nats_users_create.md create mode 100644 docs/potctl_md/potctl_nats_users_creds.md create mode 100644 docs/potctl_md/potctl_nats_users_delete-mqtt-bearer.md create mode 100644 docs/potctl_md/potctl_nats_users_delete.md create mode 100644 docs/potctl_md/potctl_prune.md create mode 100644 docs/potctl_md/potctl_prune_agent.md create mode 100644 docs/potctl_md/potctl_rebuild.md create mode 100644 docs/potctl_md/potctl_rebuild_microservice.md create mode 100644 docs/potctl_md/potctl_rebuild_system-microservice.md create mode 100644 docs/potctl_md/potctl_reconcile.md create mode 100644 docs/potctl_md/potctl_reconcile_agent.md create mode 100644 docs/potctl_md/potctl_reconcile_service.md create mode 100644 docs/potctl_md/potctl_rollback.md create mode 100644 docs/potctl_md/potctl_start.md create mode 100644 docs/potctl_md/potctl_start_application.md create mode 100644 docs/potctl_md/potctl_start_microservice.md create mode 100644 docs/potctl_md/potctl_stop.md create mode 100644 docs/potctl_md/potctl_stop_application.md create mode 100644 docs/potctl_md/potctl_stop_microservice.md create mode 100644 docs/potctl_md/potctl_upgrade.md create mode 100644 docs/potctl_md/potctl_version.md create mode 100644 docs/potctl_md/potctl_view.md create mode 100644 internal/cmd/help.go diff --git a/docs/md/iofogctl.md b/docs/iofogctl_md/iofogctl.md similarity index 95% rename from docs/md/iofogctl.md rename to docs/iofogctl_md/iofogctl.md index 44f4f3762..1d44a4cc9 100644 --- a/docs/md/iofogctl.md +++ b/docs/iofogctl_md/iofogctl.md @@ -34,6 +34,7 @@ iofogctl [flags] * [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 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 diff --git a/docs/md/iofogctl_attach.md b/docs/iofogctl_md/iofogctl_attach.md similarity index 98% rename from docs/md/iofogctl_attach.md rename to docs/iofogctl_md/iofogctl_attach.md index ab893fbc0..b3c77d36b 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 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 100% rename from docs/md/iofogctl_attach_exec.md rename to docs/iofogctl_md/iofogctl_attach_exec.md diff --git a/docs/md/iofogctl_attach_exec_agent.md b/docs/iofogctl_md/iofogctl_attach_exec_agent.md similarity index 100% rename from docs/md/iofogctl_attach_exec_agent.md rename to docs/iofogctl_md/iofogctl_attach_exec_agent.md diff --git a/docs/md/iofogctl_attach_exec_microservice.md b/docs/iofogctl_md/iofogctl_attach_exec_microservice.md similarity index 100% rename from docs/md/iofogctl_attach_exec_microservice.md rename to docs/iofogctl_md/iofogctl_attach_exec_microservice.md 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 24b5cc449..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 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 91% rename from docs/md/iofogctl_deploy.md rename to docs/iofogctl_md/iofogctl_deploy.md index 7405b7699..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,7 +14,7 @@ iofogctl deploy [flags] ### Examples ``` -deploy -f ecn.yaml +iofogctl deploy -f ecn.yaml application-template.yaml application.yaml microservice.yaml diff --git a/docs/md/iofogctl_describe.md b/docs/iofogctl_md/iofogctl_describe.md similarity index 100% rename from docs/md/iofogctl_describe.md rename to docs/iofogctl_md/iofogctl_describe.md 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 98% rename from docs/md/iofogctl_detach.md rename to docs/iofogctl_md/iofogctl_detach.md index 1cae26bf5..5d1ba6968 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 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 100% rename from docs/md/iofogctl_detach_exec.md rename to docs/iofogctl_md/iofogctl_detach_exec.md diff --git a/docs/md/iofogctl_detach_exec_agent.md b/docs/iofogctl_md/iofogctl_detach_exec_agent.md similarity index 100% rename from docs/md/iofogctl_detach_exec_agent.md rename to docs/iofogctl_md/iofogctl_detach_exec_agent.md diff --git a/docs/md/iofogctl_detach_exec_microservice.md b/docs/iofogctl_md/iofogctl_detach_exec_microservice.md similarity index 100% rename from docs/md/iofogctl_detach_exec_microservice.md rename to docs/iofogctl_md/iofogctl_detach_exec_microservice.md 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 100% rename from docs/md/iofogctl_exec.md rename to docs/iofogctl_md/iofogctl_exec.md diff --git a/docs/md/iofogctl_exec_agent.md b/docs/iofogctl_md/iofogctl_exec_agent.md similarity index 100% rename from docs/md/iofogctl_exec_agent.md rename to docs/iofogctl_md/iofogctl_exec_agent.md diff --git a/docs/md/iofogctl_exec_microservice.md b/docs/iofogctl_md/iofogctl_exec_microservice.md similarity index 100% rename from docs/md/iofogctl_exec_microservice.md rename to docs/iofogctl_md/iofogctl_exec_microservice.md diff --git a/docs/md/iofogctl_get.md b/docs/iofogctl_md/iofogctl_get.md similarity index 100% rename from docs/md/iofogctl_get.md rename to docs/iofogctl_md/iofogctl_get.md diff --git a/docs/md/iofogctl_logs.md b/docs/iofogctl_md/iofogctl_logs.md similarity index 100% rename from docs/md/iofogctl_logs.md rename to docs/iofogctl_md/iofogctl_logs.md 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 100% rename from docs/md/iofogctl_view.md rename to docs/iofogctl_md/iofogctl_view.md diff --git a/docs/potctl_md/potctl.md b/docs/potctl_md/potctl.md new file mode 100644 index 000000000..a75442e3c --- /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 ECN Viewer + + diff --git a/docs/potctl_md/potctl_attach.md b/docs/potctl_md/potctl_attach.md new file mode 100644 index 000000000..9221a18cc --- /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) - Attach an Exec Session to a resource +* [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..73beb2f76 --- /dev/null +++ b/docs/potctl_md/potctl_attach_exec.md @@ -0,0 +1,35 @@ +## potctl attach exec + +Attach an Exec Session to a resource + +### Synopsis + +Attach an Exec Session to a Microservice or Agent. + +### Examples + +``` +potctl attach exec microservice AppName/MicroserviceName +``` + +### 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) - Attach an Exec Session to an Agent +* [potctl attach exec microservice](potctl_attach_exec_microservice.md) - Attach an Exec Session to a Microservice + + 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..4719d32d7 --- /dev/null +++ b/docs/potctl_md/potctl_attach_exec_agent.md @@ -0,0 +1,37 @@ +## potctl attach exec agent + +Attach an Exec Session to an Agent + +### Synopsis + +Attach an Exec Session to an existing Agent. + +``` +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) - Attach an Exec Session to a resource + + diff --git a/docs/potctl_md/potctl_attach_exec_microservice.md b/docs/potctl_md/potctl_attach_exec_microservice.md new file mode 100644 index 000000000..ecd46e8b0 --- /dev/null +++ b/docs/potctl_md/potctl_attach_exec_microservice.md @@ -0,0 +1,37 @@ +## potctl attach exec microservice + +Attach an Exec Session to a Microservice + +### Synopsis + +Attach an Exec Session to an existing Microservice. + +``` +potctl attach exec microservice NAME [flags] +``` + +### Examples + +``` +potctl attach 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 attach exec](potctl_attach_exec.md) - Attach an Exec Session to a resource + + 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/potctl_md/potctl_delete_controller.md b/docs/potctl_md/potctl_delete_controller.md new file mode 100644 index 000000000..4b50fcacc --- /dev/null +++ b/docs/potctl_md/potctl_delete_controller.md @@ -0,0 +1,38 @@ +## potctl delete controller + +Delete a Controller + +### Synopsis + +Delete a Controller. + +``` +potctl delete controller NAME [flags] +``` + +### Examples + +``` +potctl delete controller NAME +``` + +### Options + +``` + -h, --help help for controller +``` + +### 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_microservice.md b/docs/potctl_md/potctl_delete_microservice.md new file mode 100644 index 000000000..744dcbd33 --- /dev/null +++ b/docs/potctl_md/potctl_delete_microservice.md @@ -0,0 +1,38 @@ +## potctl delete microservice + +Delete a Microservice + +### Synopsis + +Delete a Microservice + +``` +potctl delete microservice NAME [flags] +``` + +### Examples + +``` +potctl delete microservice NAME +``` + +### Options + +``` + -h, --help help for microservice +``` + +### 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_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/potctl_md/potctl_describe_agent-config.md b/docs/potctl_md/potctl_describe_agent-config.md new file mode 100644 index 000000000..2f8c028fc --- /dev/null +++ b/docs/potctl_md/potctl_describe_agent-config.md @@ -0,0 +1,38 @@ +## potctl describe agent-config + +Get detailed information about an Agent's configuration + +### Synopsis + +Get detailed information about an Agent's configuration. + +``` +potctl describe agent-config NAME [flags] +``` + +### Examples + +``` +potctl describe agent-config NAME +``` + +### Options + +``` + -h, --help help for agent-config + -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_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..302d215b1 --- /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) - Detach an Exec Session to a resource +* [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..93a0275c8 --- /dev/null +++ b/docs/potctl_md/potctl_detach_exec.md @@ -0,0 +1,35 @@ +## potctl detach exec + +Detach an Exec Session to a resource + +### Synopsis + +Detach an Exec Session to a Microservice or Agent. + +### Examples + +``` +potctl detach exec microservice AppName/MicroserviceName +``` + +### 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) - Detach an Exec Session from an Agent +* [potctl detach exec microservice](potctl_detach_exec_microservice.md) - Detach an Exec Session to a Microservice + + 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..825eadf88 --- /dev/null +++ b/docs/potctl_md/potctl_detach_exec_agent.md @@ -0,0 +1,37 @@ +## potctl detach exec agent + +Detach an Exec Session from an Agent + +### Synopsis + +Detach an Exec Session from an existing Agent. + +``` +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) - Detach an Exec Session to a resource + + diff --git a/docs/potctl_md/potctl_detach_exec_microservice.md b/docs/potctl_md/potctl_detach_exec_microservice.md new file mode 100644 index 000000000..e975d847f --- /dev/null +++ b/docs/potctl_md/potctl_detach_exec_microservice.md @@ -0,0 +1,37 @@ +## potctl detach exec microservice + +Detach an Exec Session to a Microservice + +### Synopsis + +Detach an Exec Session to an existing Microservice. + +``` +potctl detach exec microservice NAME [flags] +``` + +### Examples + +``` +potctl detach 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 detach exec](potctl_detach_exec.md) - Detach an Exec Session to a resource + + diff --git a/docs/potctl_md/potctl_detach_volume-mount.md b/docs/potctl_md/potctl_detach_volume-mount.md new file mode 100644 index 000000000..57e745197 --- /dev/null +++ b/docs/potctl_md/potctl_detach_volume-mount.md @@ -0,0 +1,37 @@ +## potctl detach volume-mount + +Detach a Volume Mount from existing Agents + +### Synopsis + +Detach a Volume Mount from existing Agents. + +``` +potctl detach volume-mount NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +potctl detach 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 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..ed5418fc0 --- /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) - Connect to an Exec Session of an Agent +* [potctl exec microservice](potctl_exec_microservice.md) - Connect to an Exec Session of 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..4ed1e6271 --- /dev/null +++ b/docs/potctl_md/potctl_exec_agent.md @@ -0,0 +1,37 @@ +## potctl exec agent + +Connect to an Exec Session of an Agent + +### Synopsis + +Connect to an Exec Session of an Agent to interact with its container. + +``` +potctl exec agent AgentName [flags] +``` + +### Examples + +``` +potctl 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 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..c4ca59a33 --- /dev/null +++ b/docs/potctl_md/potctl_exec_microservice.md @@ -0,0 +1,37 @@ +## potctl exec microservice + +Connect to an Exec Session of a Microservice + +### Synopsis + +Connect to an Exec Session of a Microservice to interact with its container. + +``` +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..e132da18d --- /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-10000) (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/potctl_md/potctl_move_agent.md b/docs/potctl_md/potctl_move_agent.md new file mode 100644 index 000000000..1022fbab9 --- /dev/null +++ b/docs/potctl_md/potctl_move_agent.md @@ -0,0 +1,38 @@ +## potctl move agent + +Move an Agent to another Namespace + +### Synopsis + +Move an Agent to another Namespace + +``` +potctl move agent NAME DEST_NAMESPACE [flags] +``` + +### Examples + +``` +potctl move agent NAME DEST_NAMESPACE +``` + +### Options + +``` + --force Move 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 move](potctl_move.md) - Move an existing resources inside the current Namespace + + diff --git a/docs/potctl_md/potctl_move_microservice.md b/docs/potctl_md/potctl_move_microservice.md new file mode 100644 index 000000000..969868327 --- /dev/null +++ b/docs/potctl_md/potctl_move_microservice.md @@ -0,0 +1,37 @@ +## potctl move microservice + +Move a Microservice to another Agent in the same Namespace + +### Synopsis + +Move a Microservice to another Agent in the same Namespace + +``` +potctl move microservice NAME AGENT_NAME [flags] +``` + +### Examples + +``` +potctl move microservice NAME AGENT_NAME +``` + +### 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 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/potctl_md/potctl_prune_agent.md b/docs/potctl_md/potctl_prune_agent.md new file mode 100644 index 000000000..da3899972 --- /dev/null +++ b/docs/potctl_md/potctl_prune_agent.md @@ -0,0 +1,38 @@ +## potctl prune agent + +Remove all dangling images from Agent + +### Synopsis + +Remove all the images which are not used by existing containers on the specified Agent + +``` +potctl prune agent NAME [flags] +``` + +### Examples + +``` +potctl prune agent NAME +``` + +### Options + +``` + --detached Specify command is to run against detached resources + -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 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/potctl_md/potctl_rebuild_microservice.md b/docs/potctl_md/potctl_rebuild_microservice.md new file mode 100644 index 000000000..b98954707 --- /dev/null +++ b/docs/potctl_md/potctl_rebuild_microservice.md @@ -0,0 +1,37 @@ +## potctl rebuild microservice + +Rebuilds a microservice + +### Synopsis + +Rebuilds a microservice + +``` +potctl rebuild microservice AppNAME/MsvcNAME [flags] +``` + +### Examples + +``` +potctl rebuild 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 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/potctl_md/potctl_rollback.md b/docs/potctl_md/potctl_rollback.md new file mode 100644 index 000000000..ed6d9fa02 --- /dev/null +++ b/docs/potctl_md/potctl_rollback.md @@ -0,0 +1,37 @@ +## potctl rollback + +Rollback ioFog resources + +### Synopsis + +Rollback ioFog resources to latest versions available. + +``` +potctl rollback RESOURCE NAME [flags] +``` + +### Examples + +``` +potctl rollback agent NAME +``` + +### Options + +``` + -h, --help help for rollback +``` + +### 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_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/potctl_md/potctl_start_application.md b/docs/potctl_md/potctl_start_application.md new file mode 100644 index 000000000..94169194f --- /dev/null +++ b/docs/potctl_md/potctl_start_application.md @@ -0,0 +1,37 @@ +## potctl start application + +Starts an application + +### Synopsis + +Starts an application + +``` +potctl start application NAME [flags] +``` + +### Examples + +``` +potctl start 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 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..a48bf760e --- /dev/null +++ b/docs/potctl_md/potctl_view.md @@ -0,0 +1,27 @@ +## potctl view + +Open ECN Viewer + +``` +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/internal/cmd/attach.go b/internal/cmd/attach.go index 5fb3867de..57fcd029d 100644 --- a/internal/cmd/attach.go +++ b/internal/cmd/attach.go @@ -7,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.`, } diff --git a/internal/cmd/attach_agent.go b/internal/cmd/attach_agent.go index 5e9789371..cb5ee7494 100644 --- a/internal/cmd/attach_agent.go +++ b/internal/cmd/attach_agent.go @@ -14,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_exec.go b/internal/cmd/attach_exec.go index c01b8569b..43f8d2df1 100644 --- a/internal/cmd/attach_exec.go +++ b/internal/cmd/attach_exec.go @@ -15,7 +15,7 @@ func NewAttachExecMicroserviceCommand() *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`, + Example: ex(`%[1]s attach exec microservice AppName/MicroserviceName`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { opt.Name = args[0] @@ -42,7 +42,7 @@ func newAttachExecAgentCommand() *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`, + 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] @@ -71,7 +71,7 @@ func newAttachExecCommand() *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`, + Example: ex(`%[1]s attach exec microservice AppName/MicroserviceName`), } // Add subcommands diff --git a/internal/cmd/attach_volume_moount.go b/internal/cmd/attach_volume_moount.go index a479a9a70..ef3dec65a 100644 --- a/internal/cmd/attach_volume_moount.go +++ b/internal/cmd/attach_volume_moount.go @@ -15,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 b1fc67cac..49c17b0b1 100644 --- a/internal/cmd/bash_complete.go +++ b/internal/cmd/bash_complete.go @@ -16,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/create_namespace.go b/internal/cmd/create_namespace.go index 40615b725..be9e2cded 100644 --- a/internal/cmd/create_namespace.go +++ b/internal/cmd/create_namespace.go @@ -10,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_agent.go b/internal/cmd/delete_agent.go index 276c80cb8..4fceb913b 100644 --- a/internal/cmd/delete_agent.go +++ b/internal/cmd/delete_agent.go @@ -11,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 262be16da..aa2776cd1 100644 --- a/internal/cmd/delete_all.go +++ b/internal/cmd/delete_all.go @@ -16,7 +16,7 @@ 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") diff --git a/internal/cmd/delete_application.go b/internal/cmd/delete_application.go index eac49a299..2bffa2b71 100644 --- a/internal/cmd/delete_application.go +++ b/internal/cmd/delete_application.go @@ -11,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 b719da4f8..8bb54a65d 100644 --- a/internal/cmd/delete_catalog_item.go +++ b/internal/cmd/delete_catalog_item.go @@ -11,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 62d20b66b..59a2bdf67 100644 --- a/internal/cmd/delete_certificate.go +++ b/internal/cmd/delete_certificate.go @@ -11,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 24ca9724d..1da91fb92 100644 --- a/internal/cmd/delete_config_map.go +++ b/internal/cmd/delete_config_map.go @@ -11,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 b4d7b8696..315d31b47 100644 --- a/internal/cmd/delete_controller.go +++ b/internal/cmd/delete_controller.go @@ -11,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_microservice.go b/internal/cmd/delete_microservice.go index f505c1b84..328571897 100644 --- a/internal/cmd/delete_microservice.go +++ b/internal/cmd/delete_microservice.go @@ -11,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 e5892b6d4..9fc8412d9 100644 --- a/internal/cmd/delete_namespace.go +++ b/internal/cmd/delete_namespace.go @@ -16,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 4ccfab39d..070449d5c 100644 --- a/internal/cmd/delete_registry.go +++ b/internal/cmd/delete_registry.go @@ -11,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 120b8ab6b..1f7a491d1 100644 --- a/internal/cmd/delete_role.go +++ b/internal/cmd/delete_role.go @@ -11,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 a7ac6690f..55cafef74 100644 --- a/internal/cmd/delete_rolebinding.go +++ b/internal/cmd/delete_rolebinding.go @@ -11,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 3ad22197e..9f13bcf48 100644 --- a/internal/cmd/delete_secret.go +++ b/internal/cmd/delete_secret.go @@ -11,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 447a3e5d7..de6baba99 100644 --- a/internal/cmd/delete_service.go +++ b/internal/cmd/delete_service.go @@ -11,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 76b5c345b..5b890fab6 100644 --- a/internal/cmd/delete_serviceaccount.go +++ b/internal/cmd/delete_serviceaccount.go @@ -11,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 1f6285454..6e4a2f996 100644 --- a/internal/cmd/delete_template.go +++ b/internal/cmd/delete_template.go @@ -11,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 de23bb522..22ee41832 100644 --- a/internal/cmd/delete_volume.go +++ b/internal/cmd/delete_volume.go @@ -13,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 95a706cbc..a339520dc 100644 --- a/internal/cmd/delete_volume_mount.go +++ b/internal/cmd/delete_volume_mount.go @@ -11,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 a0d8f1607..f684ba4ae 100644 --- a/internal/cmd/deploy.go +++ b/internal/cmd/deploy.go @@ -15,7 +15,7 @@ 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 @@ -25,12 +25,12 @@ func newDeployCommand() *cobra.Command { 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_agent.go b/internal/cmd/describe_agent.go index 3f095438e..21ab80007 100644 --- a/internal/cmd/describe_agent.go +++ b/internal/cmd/describe_agent.go @@ -15,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 2d7a5b639..d46211653 100644 --- a/internal/cmd/describe_agent_config.go +++ b/internal/cmd/describe_agent_config.go @@ -15,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 7c1000970..845ac60f6 100644 --- a/internal/cmd/describe_application.go +++ b/internal/cmd/describe_application.go @@ -15,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 749a50e73..517e812fb 100644 --- a/internal/cmd/describe_certificate.go +++ b/internal/cmd/describe_certificate.go @@ -15,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 686647676..23e919a10 100644 --- a/internal/cmd/describe_config_map.go +++ b/internal/cmd/describe_config_map.go @@ -15,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 cc76efe72..2e04677f5 100644 --- a/internal/cmd/describe_controller.go +++ b/internal/cmd/describe_controller.go @@ -15,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 1539feef7..854825a50 100644 --- a/internal/cmd/describe_controlplane.go +++ b/internal/cmd/describe_controlplane.go @@ -15,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_microservice.go b/internal/cmd/describe_microservice.go index 0e49c8c97..cc00517c0 100644 --- a/internal/cmd/describe_microservice.go +++ b/internal/cmd/describe_microservice.go @@ -15,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 172284d22..a3c44cd5d 100644 --- a/internal/cmd/describe_namespace.go +++ b/internal/cmd/describe_namespace.go @@ -15,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 6e6eba9e0..a2f14951a 100644 --- a/internal/cmd/describe_registry.go +++ b/internal/cmd/describe_registry.go @@ -15,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 27e886549..733627dd8 100644 --- a/internal/cmd/describe_role.go +++ b/internal/cmd/describe_role.go @@ -15,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 c38969110..fe67549eb 100644 --- a/internal/cmd/describe_rolebinding.go +++ b/internal/cmd/describe_rolebinding.go @@ -15,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 8c14636e1..3be8fc5ff 100644 --- a/internal/cmd/describe_secret.go +++ b/internal/cmd/describe_secret.go @@ -15,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 a35d67a26..a3fb06ccd 100644 --- a/internal/cmd/describe_service.go +++ b/internal/cmd/describe_service.go @@ -15,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 d3af0cbc1..32679eaea 100644 --- a/internal/cmd/describe_serviceaccount.go +++ b/internal/cmd/describe_serviceaccount.go @@ -15,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 828d78253..c671a2f83 100644 --- a/internal/cmd/describe_system_microservice.go +++ b/internal/cmd/describe_system_microservice.go @@ -15,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 ed18feff3..1bf6aedd0 100644 --- a/internal/cmd/describe_template.go +++ b/internal/cmd/describe_template.go @@ -15,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 22f4088d0..3979440b3 100644 --- a/internal/cmd/describe_volume.go +++ b/internal/cmd/describe_volume.go @@ -15,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 83587a255..98ab735b8 100644 --- a/internal/cmd/describe_volume_mount.go +++ b/internal/cmd/describe_volume_mount.go @@ -15,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 1785765e7..1a74f3ecb 100644 --- a/internal/cmd/detach.go +++ b/internal/cmd/detach.go @@ -7,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.`, } diff --git a/internal/cmd/detach_agent.go b/internal/cmd/detach_agent.go index da46fb3b7..8042c9a5a 100644 --- a/internal/cmd/detach_agent.go +++ b/internal/cmd/detach_agent.go @@ -19,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_exec.go b/internal/cmd/detach_exec.go index e2818df91..b32fb281f 100644 --- a/internal/cmd/detach_exec.go +++ b/internal/cmd/detach_exec.go @@ -15,7 +15,7 @@ func NewDetachExecMicroserviceCommand() *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`, + Example: ex(`%[1]s detach exec microservice AppName/MicroserviceName`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { opt.Name = args[0] @@ -42,7 +42,7 @@ func newDetachExecAgentCommand() *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`, + Example: ex(`%[1]s detach exec agent AgentName`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { opt.Name = args[0] @@ -68,7 +68,7 @@ func newDetachExecCommand() *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`, + Example: ex(`%[1]s detach exec microservice AppName/MicroserviceName`), } // Add subcommands diff --git a/internal/cmd/detach_volume_mount.go b/internal/cmd/detach_volume_mount.go index 953d53fa5..ddeb84aab 100644 --- a/internal/cmd/detach_volume_mount.go +++ b/internal/cmd/detach_volume_mount.go @@ -15,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 9ce4a66fd..df2bdaeaf 100644 --- a/internal/cmd/disconnect.go +++ b/internal/cmd/disconnect.go @@ -19,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_agent.go b/internal/cmd/exec_agent.go index cea8a3ee2..9ddafe1e2 100644 --- a/internal/cmd/exec_agent.go +++ b/internal/cmd/exec_agent.go @@ -15,7 +15,7 @@ func newExecAgentCommand() *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`, + Example: ex(`%[1]s exec agent AgentName`), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get resource type and name diff --git a/internal/cmd/exec_microservice.go b/internal/cmd/exec_microservice.go index 15870dace..e11a6b295 100644 --- a/internal/cmd/exec_microservice.go +++ b/internal/cmd/exec_microservice.go @@ -15,7 +15,7 @@ func newExecMicroserviceCommand() *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`, + 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 cf6914890..50f28e96a 100644 --- a/internal/cmd/generate_documentation.go +++ b/internal/cmd/generate_documentation.go @@ -20,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 331570fc3..036e8c057 100644 --- a/internal/cmd/get.go +++ b/internal/cmd/get.go @@ -42,7 +42,7 @@ 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 @@ -65,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/logs.go b/internal/cmd/logs.go index c1cd64b20..7c42a84c7 100644 --- a/internal/cmd/logs.go +++ b/internal/cmd/logs.go @@ -11,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 diff --git a/internal/cmd/move_agent.go b/internal/cmd/move_agent.go index eeb04b045..bf9b76b0b 100644 --- a/internal/cmd/move_agent.go +++ b/internal/cmd/move_agent.go @@ -14,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 ff3d8f080..7d6c17c8d 100644 --- a/internal/cmd/move_microservice.go +++ b/internal/cmd/move_microservice.go @@ -11,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/prune_agent.go b/internal/cmd/prune_agent.go index be53d7a6d..e28407ecf 100644 --- a/internal/cmd/prune_agent.go +++ b/internal/cmd/prune_agent.go @@ -11,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_microservice.go b/internal/cmd/rebuild_microservice.go index ce186291f..8018976cb 100644 --- a/internal/cmd/rebuild_microservice.go +++ b/internal/cmd/rebuild_microservice.go @@ -12,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 6bd592502..785c5c07b 100644 --- a/internal/cmd/rebuild_system_microservice.go +++ b/internal/cmd/rebuild_system_microservice.go @@ -12,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 index 4315e5fc4..c7d7bbbe6 100644 --- a/internal/cmd/reconcile.go +++ b/internal/cmd/reconcile.go @@ -25,7 +25,7 @@ func newReconcileAgentCommand() *cobra.Command { cmd := &cobra.Command{ Use: "agent NAME", Short: "Reconcile fog router/NATS platform for an agent", - Example: "iofogctl reconcile agent my-agent", + Example: ex("%[1]s reconcile agent my-agent"), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { name := args[0] @@ -44,7 +44,7 @@ func newReconcileServiceCommand() *cobra.Command { cmd := &cobra.Command{ Use: "service NAME", Short: "Reconcile service hub provisioning", - Example: "iofogctl reconcile service my-service", + Example: ex("%[1]s reconcile service my-service"), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { name := args[0] diff --git a/internal/cmd/rollback.go b/internal/cmd/rollback.go index d194f07d8..671be648b 100644 --- a/internal/cmd/rollback.go +++ b/internal/cmd/rollback.go @@ -17,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 diff --git a/internal/cmd/start_application.go b/internal/cmd/start_application.go index d6ddd85a1..3bd1ff176 100644 --- a/internal/cmd/start_application.go +++ b/internal/cmd/start_application.go @@ -12,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 2b7ab8202..168e1053f 100644 --- a/internal/cmd/start_microservice.go +++ b/internal/cmd/start_microservice.go @@ -12,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_application.go b/internal/cmd/stop_application.go index 2974490b3..3ae4c6228 100644 --- a/internal/cmd/stop_application.go +++ b/internal/cmd/stop_application.go @@ -12,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 38d1ae982..67b14d8ff 100644 --- a/internal/cmd/stop_microservice.go +++ b/internal/cmd/stop_microservice.go @@ -12,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 91f79d4f0..8c3ff1757 100644 --- a/internal/cmd/upgrade.go +++ b/internal/cmd/upgrade.go @@ -17,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 From ff13c4d329308de4c7cecd473206a5a05882b0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 15:23:58 +0300 Subject: [PATCH 50/63] Validate edgelet download URLs and harden remote deploy transfers. Restrict edgelet fetches to the configured release base and adopt safe path helpers across airgap, offline image, and remote deploy flows. --- assets/edgelet/scripts/install.sh | 6 +- internal/delete/volume/remote.go | 1 + internal/deploy/agent/edgelet.go | 3 +- internal/deploy/agent/local.go | 9 ++- internal/deploy/agent/remote.go | 7 +- internal/deploy/airgap/agent_edgelet.go | 5 +- internal/deploy/airgap/binary.go | 8 +-- internal/deploy/airgap/binary_test.go | 4 +- internal/deploy/airgap/cache.go | 6 +- internal/deploy/airgap/images.go | 42 ++---------- internal/deploy/airgap/transfer.go | 20 +++--- internal/deploy/catalogitem/catalog_item.go | 2 +- .../deploy/controlplane/local/system_agent.go | 8 ++- .../deploy/controlplane/local/translate.go | 4 ++ .../controlplane/remote/system_agent.go | 8 ++- .../deploy/controlplane/remote/translate.go | 4 ++ internal/deploy/offlineimage/images.go | 20 +++--- internal/deploy/offlineimage/transfer.go | 5 +- internal/deploy/validate/remote_controller.go | 15 +++-- internal/deploy/volume/remote.go | 1 + internal/resource/edgelet_golden_test.go | 20 +++--- pkg/iofog/install/agent.go | 15 ++--- pkg/iofog/install/edgelet_config.go | 8 ++- pkg/iofog/install/edgelet_deploy.go | 10 +-- pkg/iofog/install/edgelet_local.go | 15 +++-- pkg/iofog/install/edgelet_procedures_test.go | 6 +- pkg/iofog/install/edgelet_remote.go | 8 +-- pkg/iofog/install/edgelet_remote_test.go | 7 +- pkg/iofog/install/procedures_test.go | 4 +- pkg/util/edgelet_binary.go | 65 +++++++++++++++++-- pkg/util/edgelet_binary_test.go | 8 +-- versions.mk | 2 +- 32 files changed, 202 insertions(+), 144 deletions(-) diff --git a/assets/edgelet/scripts/install.sh b/assets/edgelet/scripts/install.sh index 38e2858f5..2b16f661b 100644 --- a/assets/edgelet/scripts/install.sh +++ b/assets/edgelet/scripts/install.sh @@ -2,9 +2,9 @@ # install.sh — Edgelet installer (potctl chunked fork; upstream parity for upgrade/rollback) # # Usage: -# sudo ./install.sh --version=v1.0.0-rc.4 -# sudo ./install.sh --airgap --bin-path=/path/to/edgelet-linux-amd64 --version=v1.0.0-rc.4 -# sudo ./install.sh --upgrade --version=v1.0.0-rc.4 +# sudo ./install.sh --version=v1.0.0-rc.5 +# sudo ./install.sh --airgap --bin-path=/path/to/edgelet-linux-amd64 --version=v1.0.0-rc.5 +# sudo ./install.sh --upgrade --version=v1.0.0-rc.5 # sudo ./install.sh --rollback # # potctl deploy uses --skip-config and --skip-start (config/start handled by iofogctl/potctl). diff --git a/internal/delete/volume/remote.go b/internal/delete/volume/remote.go index f23182091..a3df84656 100644 --- a/internal/delete/volume/remote.go +++ b/internal/delete/volume/remote.go @@ -19,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/deploy/agent/edgelet.go b/internal/deploy/agent/edgelet.go index a90863424..a3c9700ae 100644 --- a/internal/deploy/agent/edgelet.go +++ b/internal/deploy/agent/edgelet.go @@ -3,6 +3,7 @@ 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" @@ -11,7 +12,7 @@ import ( type edgeletAgent interface { Bootstrap() error - Configure(controllerEndpoint string, user install.IofogUser) (string, error) + Configure(controllerEndpoint string, user install.IofogUser, sdkOpt client.Options) (string, error) SetVersion(version string) error SetContainerImage(image string) error SetAirgap(binPath string) error diff --git a/internal/deploy/agent/local.go b/internal/deploy/agent/local.go index beb7e0139..0658e92a0 100644 --- a/internal/deploy/agent/local.go +++ b/internal/deploy/agent/local.go @@ -1,9 +1,12 @@ package deployagent import ( + "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" ) @@ -69,7 +72,11 @@ func (exe *localExecutor) ProvisionAgent() (string, error) { user := install.IofogUser(controlPlane.GetUser()) user.Password = controlPlane.GetUser().GetRawPassword() - return edgelet.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 { diff --git a/internal/deploy/agent/remote.go b/internal/deploy/agent/remote.go index b6c242f5b..37a9b38e2 100644 --- a/internal/deploy/agent/remote.go +++ b/internal/deploy/agent/remote.go @@ -7,6 +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" "github.com/eclipse-iofog/iofogctl/pkg/iofog" "github.com/eclipse-iofog/iofogctl/pkg/iofog/install" "github.com/eclipse-iofog/iofogctl/pkg/util" @@ -78,7 +79,11 @@ func (exe *remoteExecutor) ProvisionAgent() (string, error) { user := install.IofogUser(controlPlane.GetUser()) user.Password = controlPlane.GetUser().GetRawPassword() - return edgelet.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 *remoteExecutor) Execute() (err error) { diff --git a/internal/deploy/airgap/agent_edgelet.go b/internal/deploy/airgap/agent_edgelet.go index e0ada3eef..a30b74e79 100644 --- a/internal/deploy/airgap/agent_edgelet.go +++ b/internal/deploy/airgap/agent_edgelet.go @@ -22,10 +22,7 @@ func EnsureAgentConfig(agent *rsc.AgentConfiguration) *rsc.AgentConfiguration { func ResolveAgentDeployment(cfg *rsc.AgentConfiguration, containerImage string) bool { cfg = EnsureAgentConfig(cfg) - useContainer := false - if cfg.DeploymentType != nil && strings.EqualFold(strings.TrimSpace(*cfg.DeploymentType), DeploymentTypeContainer) { - useContainer = true - } + useContainer := cfg.DeploymentType != nil && strings.EqualFold(strings.TrimSpace(*cfg.DeploymentType), DeploymentTypeContainer) if containerImage != "" { useContainer = true } diff --git a/internal/deploy/airgap/binary.go b/internal/deploy/airgap/binary.go index 856417246..8e7b4def2 100644 --- a/internal/deploy/airgap/binary.go +++ b/internal/deploy/airgap/binary.go @@ -28,7 +28,7 @@ type binaryCacheMetadata struct { 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, 0o755); err != nil { + if err := os.MkdirAll(cacheDir, util.DirPerm); err != nil { return "", err } @@ -68,7 +68,7 @@ func EnsureEdgeletBinary(_ context.Context, namespace, osName, archName string) } func loadBinaryCacheMetadata(path string) (*binaryCacheMetadata, error) { - data, err := os.ReadFile(path) + data, err := util.ReadValidatedFile(path) if err != nil { return nil, err } @@ -84,7 +84,7 @@ func saveBinaryCacheMetadata(path string, meta binaryCacheMetadata) error { if err != nil { return err } - return os.WriteFile(path, data, 0o644) + return util.WriteValidatedFile(path, data, util.FilePerm) } func canReuseCachedBinary(path string, cached *binaryCacheMetadata) (bool, string) { @@ -143,7 +143,7 @@ func TransferAirgapBinary(host string, ssh *rsc.SSH, osName, archName, localPath return "", err } - file, err := os.Open(localPath) + file, err := util.OpenValidatedFile(localPath) if err != nil { return "", err } diff --git a/internal/deploy/airgap/binary_test.go b/internal/deploy/airgap/binary_test.go index aed9e4115..b6a050609 100644 --- a/internal/deploy/airgap/binary_test.go +++ b/internal/deploy/airgap/binary_test.go @@ -15,7 +15,7 @@ import ( 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.4/edgelet-linux-amd64" { + if r.URL.Path != "/v1.0.0-rc.5/edgelet-linux-amd64" { http.NotFound(w, r) return } @@ -24,7 +24,7 @@ func TestEnsureEdgeletBinaryUsesCache(t *testing.T) { defer server.Close() util.SetEdgeletReleaseBaseForTest(server.URL) - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.4") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.5") t.Cleanup(func() { util.ResetEdgeletReleaseBaseForTest() util.ResetEdgeletBinaryVersionForTest() diff --git a/internal/deploy/airgap/cache.go b/internal/deploy/airgap/cache.go index 361024a6e..be7f6e975 100644 --- a/internal/deploy/airgap/cache.go +++ b/internal/deploy/airgap/cache.go @@ -10,6 +10,8 @@ import ( "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/images.go b/internal/deploy/airgap/images.go index 2466b6eaa..f7ffc3176 100644 --- a/internal/deploy/airgap/images.go +++ b/internal/deploy/airgap/images.go @@ -165,32 +165,16 @@ func applyYAMLAndUtilFallbackForController(images *RequiredImages, controlPlane } } if images.DebuggerAMD64 == "" { - if images.DebuggerAMD64 != "" { - images.DebuggerAMD64 = images.DebuggerAMD64 - } else { - images.DebuggerAMD64 = util.GetDebuggerImage() - } + images.DebuggerAMD64 = util.GetDebuggerImage() } if images.DebuggerARM64 == "" { - if images.DebuggerARM64 != "" { - images.DebuggerARM64 = images.DebuggerARM64 - } else { - images.DebuggerARM64 = util.GetDebuggerImage() - } + images.DebuggerARM64 = util.GetDebuggerImage() } if images.DebuggerRISCV64 == "" { - if images.DebuggerRISCV64 != "" { - images.DebuggerRISCV64 = images.DebuggerRISCV64 - } else { - images.DebuggerRISCV64 = util.GetDebuggerImage() - } + images.DebuggerRISCV64 = util.GetDebuggerImage() } if images.DebuggerARM == "" { - if images.DebuggerARM != "" { - images.DebuggerARM = images.DebuggerARM - } else { - images.DebuggerARM = util.GetDebuggerImage() - } + images.DebuggerARM = util.GetDebuggerImage() } } @@ -229,25 +213,13 @@ func applyYAMLAndUtilFallbackForAgent(images *RequiredImages, controlPlane *rsc. images.DebuggerAMD64 = util.GetDebuggerImage() } if images.DebuggerARM64 == "" { - if images.DebuggerARM64 != "" { - images.DebuggerARM64 = images.DebuggerARM64 - } else { - images.DebuggerARM64 = util.GetDebuggerImage() - } + images.DebuggerARM64 = util.GetDebuggerImage() } if images.DebuggerRISCV64 == "" { - if images.DebuggerRISCV64 != "" { - images.DebuggerRISCV64 = images.DebuggerRISCV64 - } else { - images.DebuggerRISCV64 = util.GetDebuggerImage() - } + images.DebuggerRISCV64 = util.GetDebuggerImage() } if images.DebuggerARM == "" { - if images.DebuggerARM != "" { - images.DebuggerARM = images.DebuggerARM - } else { - images.DebuggerARM = util.GetDebuggerImage() - } + images.DebuggerARM = util.GetDebuggerImage() } } diff --git a/internal/deploy/airgap/transfer.go b/internal/deploy/airgap/transfer.go index 5dafd837c..5f79d63af 100644 --- a/internal/deploy/airgap/transfer.go +++ b/internal/deploy/airgap/transfer.go @@ -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" @@ -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 diff --git a/internal/deploy/catalogitem/catalog_item.go b/internal/deploy/catalogitem/catalog_item.go index d05293e5f..f0d5060ab 100644 --- a/internal/deploy/catalogitem/catalog_item.go +++ b/internal/deploy/catalogitem/catalog_item.go @@ -159,7 +159,7 @@ func validate(opt *apps.CatalogItem) error { return err } - if opt.ARM == "" && opt.ARM64 == "" && opt.RISCV64 == "" && opt.ARM == "" { + if opt.AMD64 == "" && opt.ARM64 == "" && opt.RISCV64 == "" && opt.ARM == "" { return util.NewInputError("At least one image must be specified") } diff --git a/internal/deploy/controlplane/local/system_agent.go b/internal/deploy/controlplane/local/system_agent.go index e4ce6773f..62facdc99 100644 --- a/internal/deploy/controlplane/local/system_agent.go +++ b/internal/deploy/controlplane/local/system_agent.go @@ -1,6 +1,7 @@ package deploylocalcontrolplane import ( + "context" "fmt" "strings" @@ -89,7 +90,12 @@ func deployLocalSystemAgent(namespace string, cp *rsc.LocalControlPlane, name st return err } - if _, err := edgelet.Configure(endpoint, user); err != nil { + 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()) diff --git a/internal/deploy/controlplane/local/translate.go b/internal/deploy/controlplane/local/translate.go index 01a87dc81..a10defb02 100644 --- a/internal/deploy/controlplane/local/translate.go +++ b/internal/deploy/controlplane/local/translate.go @@ -112,11 +112,15 @@ func TranslateLocalControlPlane(cp *resource.LocalControlPlane, opts TranslateOp } // 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") diff --git a/internal/deploy/controlplane/remote/system_agent.go b/internal/deploy/controlplane/remote/system_agent.go index fecb13155..2316b115d 100644 --- a/internal/deploy/controlplane/remote/system_agent.go +++ b/internal/deploy/controlplane/remote/system_agent.go @@ -1,6 +1,7 @@ package deployremotecontrolplane import ( + "context" "fmt" "strings" @@ -9,6 +10,7 @@ import ( 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" ) @@ -170,7 +172,11 @@ func deployRemoteSystemAgent(namespace string, cp *rsc.RemoteControlPlane, ctrl user := install.IofogUser(cp.GetUser()) user.Password = cp.GetUser().GetRawPassword() - if _, err := edgelet.Configure(endpoint, user); err != nil { + 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) } diff --git a/internal/deploy/controlplane/remote/translate.go b/internal/deploy/controlplane/remote/translate.go index 6f989a173..543fa6a5c 100644 --- a/internal/deploy/controlplane/remote/translate.go +++ b/internal/deploy/controlplane/remote/translate.go @@ -122,11 +122,15 @@ func TranslateRemoteControlPlane(cp *rsc.RemoteControlPlane, ctrl *rsc.RemoteCon } // 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") diff --git a/internal/deploy/offlineimage/images.go b/internal/deploy/offlineimage/images.go index d61885385..1732b9137 100644 --- a/internal/deploy/offlineimage/images.go +++ b/internal/deploy/offlineimage/images.go @@ -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" @@ -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/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/validate/remote_controller.go b/internal/deploy/validate/remote_controller.go index 7283cbef6..fdb1352ef 100644 --- a/internal/deploy/validate/remote_controller.go +++ b/internal/deploy/validate/remote_controller.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "os" "github.com/eclipse-iofog/iofogctl/internal/config" "github.com/eclipse-iofog/iofogctl/pkg/util" @@ -17,18 +16,20 @@ type deployControllerRef struct { host string } +type deployDocMetadata struct { + Name string `yaml:"name"` +} + type deployDocHeader struct { - Kind config.Kind `yaml:"kind"` - Metadata struct { - Name string `yaml:"name"` - } `yaml:"metadata"` - Spec map[string]interface{} `yaml:"spec"` + 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 := os.ReadFile(inputFile) + yamlFile, err := util.ReadUserFile(inputFile) if err != nil { return err } diff --git a/internal/deploy/volume/remote.go b/internal/deploy/volume/remote.go index c2f222e61..9a8c083de 100644 --- a/internal/deploy/volume/remote.go +++ b/internal/deploy/volume/remote.go @@ -46,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/resource/edgelet_golden_test.go b/internal/resource/edgelet_golden_test.go index c884f6a99..9a7fc5037 100644 --- a/internal/resource/edgelet_golden_test.go +++ b/internal/resource/edgelet_golden_test.go @@ -40,12 +40,12 @@ func assertGoldenAgentConfig(t *testing.T, cfg *AgentConfiguration) { 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.RouterConfig.RouterMode) - require.Equal(t, "edge", *cfg.RouterConfig.RouterMode) - require.NotNil(t, cfg.RouterConfig.MessagingPort) - require.Equal(t, 5671, *cfg.RouterConfig.MessagingPort) - require.NotNil(t, cfg.NatsConfig.NatsMode) - require.Equal(t, "leaf", *cfg.NatsConfig.NatsMode) + 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) } @@ -102,10 +102,10 @@ func TestGoldenUnmarshalAgentConfiguration(t *testing.T) { require.Equal(t, "riscv64", *cfg.Arch) require.NotNil(t, cfg.ContainerEngine) require.Equal(t, "edgelet", *cfg.ContainerEngine) - require.NotNil(t, cfg.RouterConfig.RouterMode) - require.Equal(t, "edge", *cfg.RouterConfig.RouterMode) - require.NotNil(t, cfg.NatsConfig.NatsMode) - require.Equal(t, "leaf", *cfg.NatsConfig.NatsMode) + 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) } diff --git a/pkg/iofog/install/agent.go b/pkg/iofog/install/agent.go index 7b28ed0ed..c2abef7d0 100644 --- a/pkg/iofog/install/agent.go +++ b/pkg/iofog/install/agent.go @@ -7,11 +7,11 @@ 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) (string, string, error) +var getProvisionKeyHook func(agent *defaultAgent, controllerEndpoint string, user IofogUser, sdkOpt client.Options) (string, string, error) // defaultAgent implements commong behavior type defaultAgent struct { @@ -19,18 +19,13 @@ type defaultAgent struct { uuid string } -func (agent *defaultAgent) getProvisionKey(controllerEndpoint string, user IofogUser) (key string, caCert string, err error) { +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) - } - // Connect to controller - baseURL, err := util.GetBaseURL(controllerEndpoint) - if err != nil { - return + 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/edgelet_config.go b/pkg/iofog/install/edgelet_config.go index badeda932..7150c488c 100644 --- a/pkg/iofog/install/edgelet_config.go +++ b/pkg/iofog/install/edgelet_config.go @@ -463,7 +463,7 @@ func runtimeConfigDir(paths EdgeletPlatformPaths) string { func ensureLocalRuntimeConfigDir(paths EdgeletPlatformPaths, useSudo bool) error { dir := runtimeConfigDir(paths) if !useSudo { - return os.MkdirAll(dir, 0o755) + return os.MkdirAll(dir, util.DirPerm) } if _, err := util.Exec("", "sudo", "mkdir", "-p", dir); err != nil { return err @@ -499,7 +499,7 @@ func writeLocalFileIfMissing(path string, content []byte, perm os.FileMode, useS return nil } - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil && !useSudo { + if err := os.MkdirAll(filepath.Dir(path), util.DirPerm); err != nil && !useSudo { return err } @@ -515,7 +515,7 @@ func writeLocalFileIfMissing(path string, content []byte, perm os.FileMode, useS defer os.Remove(tmpPath) if _, err := tmp.Write(content); err != nil { - tmp.Close() + util.IgnoreClose(tmp) return err } if err := tmp.Close(); err != nil { @@ -574,6 +574,8 @@ func MaterializeEdgeletRuntime(hostOS string, spec *EdgeletRuntimeSpec) error { } // 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") diff --git a/pkg/iofog/install/edgelet_deploy.go b/pkg/iofog/install/edgelet_deploy.go index faf682fe8..84e2f5fbd 100644 --- a/pkg/iofog/install/edgelet_deploy.go +++ b/pkg/iofog/install/edgelet_deploy.go @@ -41,7 +41,7 @@ func WriteTempManifest(data []byte, prefix string, cfg EdgeletInstallConfig) (pa dir := "" if IsDesktopContainerDeploy(cfg) { dir = EdgeletContainerManifestDir - if err := os.MkdirAll(dir, 0o755); err != nil { + if err := os.MkdirAll(dir, util.DirPerm); err != nil { return "", nil, err } } @@ -52,15 +52,15 @@ func WriteTempManifest(data []byte, prefix string, cfg EdgeletInstallConfig) (pa } path = f.Name() if _, err = f.Write(data); err != nil { - f.Close() - os.Remove(path) + util.IgnoreClose(f) + util.IgnoreErr(os.Remove(path)) return "", nil, err } if err = f.Close(); err != nil { - os.Remove(path) + util.IgnoreErr(os.Remove(path)) return "", nil, err } - return path, func() { _ = os.Remove(path) }, nil + return path, func() { util.IgnoreErr(os.Remove(path)) }, nil } // WriteDeployManifest writes a manifest using this edgelet's install config. diff --git a/pkg/iofog/install/edgelet_local.go b/pkg/iofog/install/edgelet_local.go index 0b14299b2..948de75e9 100644 --- a/pkg/iofog/install/edgelet_local.go +++ b/pkg/iofog/install/edgelet_local.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/pkg/util" ) @@ -49,7 +50,7 @@ func (agent *LocalEdgelet) CustomizeProcedures(dir string, procs *EdgeletProcedu continue } procs.scriptNames = append(procs.scriptNames, file.Name()) - content, err := os.ReadFile(filepath.Join(dir, file.Name())) + content, err := util.ReadFileUnderRoot(dir, file.Name()) if err != nil { return err } @@ -232,8 +233,8 @@ func (agent *LocalEdgelet) edgeletCommand(cmd, msg string) string { return agent.cfg.bootstrapEnv(true) + " " + prefix + cmd } -func (agent *LocalEdgelet) Configure(controllerEndpoint string, user IofogUser) (string, error) { - key, caCert, err := agent.getProvisionKey(controllerEndpoint, user) +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 } @@ -255,18 +256,18 @@ func needsLocalSudo(cfg EdgeletInstallConfig) bool { } func (agent *LocalEdgelet) materializeScripts() error { - if err := os.MkdirAll(agent.dir, 0o755); err != nil { + if err := os.MkdirAll(agent.dir, util.DirPerm); err != nil { return err } - if err := os.MkdirAll(filepath.Join(agent.dir, "lib"), 0o755); err != nil { + 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), 0o755); err != nil { + if err := os.MkdirAll(filepath.Dir(path), util.DirPerm); err != nil { return err } - if err := os.WriteFile(path, []byte(agent.procs.scriptContents[idx]), 0o755); err != nil { + if err := os.WriteFile(path, []byte(agent.procs.scriptContents[idx]), util.ExecPerm); err != nil { // #nosec G306 -- executable install scripts return err } } diff --git a/pkg/iofog/install/edgelet_procedures_test.go b/pkg/iofog/install/edgelet_procedures_test.go index e9ca144f6..611042f17 100644 --- a/pkg/iofog/install/edgelet_procedures_test.go +++ b/pkg/iofog/install/edgelet_procedures_test.go @@ -11,7 +11,7 @@ import ( func TestDefaultEdgeletProceduresScripts(t *testing.T) { util.SetEdgeletReleaseBaseForTest("https://github.com/Datasance/edgelet/releases/download") - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.4") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.5") t.Cleanup(func() { util.ResetEdgeletReleaseBaseForTest() util.ResetEdgeletBinaryVersionForTest() @@ -39,7 +39,7 @@ func TestDefaultEdgeletProceduresScripts(t *testing.T) { 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.4") { + if !strings.Contains(joined, "--version=v1.0.0-rc.5") { t.Fatalf("expected --version flag in install args, got %v", procs.Install.Args) } if !strings.Contains(joined, "--skip-start") { @@ -132,7 +132,7 @@ func TestInstallDepsSkipMatrix(t *testing.T) { func TestBootstrapCommandGeneration(t *testing.T) { util.SetEdgeletReleaseBaseForTest("https://example.com/download") - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.4") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.5") cfg := EdgeletInstallConfig{HostOS: "linux", Arch: "amd64", ContainerEngine: "docker"} procs, err := newDefaultEdgeletProcedures(EdgeletScriptStageDir, cfg) diff --git a/pkg/iofog/install/edgelet_remote.go b/pkg/iofog/install/edgelet_remote.go index 1d408cf76..b9e65a7fd 100644 --- a/pkg/iofog/install/edgelet_remote.go +++ b/pkg/iofog/install/edgelet_remote.go @@ -67,7 +67,7 @@ func (agent *RemoteEdgelet) CustomizeProcedures(dir string, procs *EdgeletProced continue } procs.scriptNames = append(procs.scriptNames, file.Name()) - content, err := os.ReadFile(filepath.Join(dir, file.Name())) + content, err := util.ReadFileUnderRoot(dir, file.Name()) if err != nil { return err } @@ -230,8 +230,8 @@ func (agent *RemoteEdgelet) Bootstrap() error { return agent.run(agent.procs.postInstallCommands(agent.name, agent.cfg, true)) } -func (agent *RemoteEdgelet) Configure(controllerEndpoint string, user IofogUser) (string, error) { - key, caCert, err := agent.getProvisionKey(controllerEndpoint, user) +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 } @@ -370,7 +370,7 @@ func (agent *RemoteEdgelet) copyLocalFileToRemote(localPath, remotePath string) if remoteEdgeletRunHook != nil { return nil } - content, err := os.ReadFile(localPath) + content, err := util.ReadValidatedFile(localPath) if err != nil { return err } diff --git a/pkg/iofog/install/edgelet_remote_test.go b/pkg/iofog/install/edgelet_remote_test.go index b20b66518..3c0e8d533 100644 --- a/pkg/iofog/install/edgelet_remote_test.go +++ b/pkg/iofog/install/edgelet_remote_test.go @@ -4,12 +4,13 @@ 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.4") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.5") t.Cleanup(func() { util.ResetEdgeletReleaseBaseForTest() util.ResetEdgeletBinaryVersionForTest() @@ -77,12 +78,12 @@ func TestRemoteEdgeletConfigureUsesMockedSSH(t *testing.T) { cfg: EdgeletInstallConfig{DeploymentType: "native"}, } - getProvisionKeyHook = func(*defaultAgent, string, IofogUser) (string, string, error) { + 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{}); err != nil { + if _, err := agent.Configure("https://controller.example.com", IofogUser{}, client.Options{}); err != nil { t.Fatalf("Configure: %v", err) } if len(ran) != 3 { diff --git a/pkg/iofog/install/procedures_test.go b/pkg/iofog/install/procedures_test.go index 7d9dc0407..d5b7d174d 100644 --- a/pkg/iofog/install/procedures_test.go +++ b/pkg/iofog/install/procedures_test.go @@ -5,10 +5,10 @@ import "testing" func TestEntrypointGetCommandQuotesInstallArgs(t *testing.T) { ep := Entrypoint{ destPath: "/tmp/edgelet-scripts/install.sh", - Args: []string{"--version=v1.0.0-rc.4", "--arch=arm64", "--skip-start"}, + Args: []string{"--version=v1.0.0-rc.5", "--arch=arm64", "--skip-start"}, } got := ep.getCommand() - want := "/tmp/edgelet-scripts/install.sh '--version=v1.0.0-rc.4' '--arch=arm64' '--skip-start'" + want := "/tmp/edgelet-scripts/install.sh '--version=v1.0.0-rc.5' '--arch=arm64' '--skip-start'" if got != want { t.Fatalf("getCommand() = %q, want %q", got, want) } diff --git a/pkg/util/edgelet_binary.go b/pkg/util/edgelet_binary.go index 4d292bafd..53511e747 100644 --- a/pkg/util/edgelet_binary.go +++ b/pkg/util/edgelet_binary.go @@ -1,14 +1,21 @@ 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": {}, @@ -104,32 +111,78 @@ func ShouldSkipInstallDeps(containerEngine, deploymentType string) bool { 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 := "/" + 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 { - url, err := EdgeletBinaryURL(osName, archName) + 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 := http.Get(url) + 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, url) + return fmt.Errorf("download edgelet binary: HTTP %d from %s", resp.StatusCode, downloadURL) } - if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { + if err := os.MkdirAll(filepath.Dir(destPath), DirPerm); err != nil { return fmt.Errorf("create download dir: %w", err) } - out, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) + out, err := CreateUserFile(destPath, ExecPerm) // #nosec G302 -- executable edgelet binary if err != nil { return fmt.Errorf("create edgelet binary file: %w", err) } - defer out.Close() + defer IgnoreClose(out) if _, err := io.Copy(out, resp.Body); err != nil { return fmt.Errorf("write edgelet binary: %w", err) diff --git a/pkg/util/edgelet_binary_test.go b/pkg/util/edgelet_binary_test.go index 37641bb91..507dfb831 100644 --- a/pkg/util/edgelet_binary_test.go +++ b/pkg/util/edgelet_binary_test.go @@ -46,13 +46,13 @@ func TestEdgeletBinaryArtifact(t *testing.T) { func TestEdgeletBinaryURL(t *testing.T) { edgeletReleaseBase = "https://github.com/Datasance/edgelet/releases/download" - edgeletBinaryVersion = "v1.0.0-rc.4" + edgeletBinaryVersion = "v1.0.0-rc.5" 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.4/edgelet-linux-amd64" + want := "https://github.com/Datasance/edgelet/releases/download/v1.0.0-rc.5/edgelet-linux-amd64" if got != want { t.Fatalf("EdgeletBinaryURL = %q, want %q", got, want) } @@ -80,7 +80,7 @@ func TestShouldSkipInstallDeps(t *testing.T) { 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.4/edgelet-linux-amd64" { + if r.URL.Path != "/v1.0.0-rc.5/edgelet-linux-amd64" { http.NotFound(w, r) return } @@ -89,7 +89,7 @@ func TestDownloadEdgeletBinary(t *testing.T) { defer server.Close() edgeletReleaseBase = server.URL - edgeletBinaryVersion = "v1.0.0-rc.4" + edgeletBinaryVersion = "v1.0.0-rc.5" dir := t.TempDir() dest := filepath.Join(dir, "edgelet-linux-amd64") diff --git a/versions.mk b/versions.mk index c867e42db..72511e208 100644 --- a/versions.mk +++ b/versions.mk @@ -2,5 +2,5 @@ OPERATOR_VERSION ?= 3.8.0-rc.1 CONTROLLER_VERSION ?= 3.8.0-rc.4 ROUTER_VERSION ?= 3.8.0-rc.1 NATS_VERSION ?= 2.14.2-rc.2 -EDGELET_BINARY_VERSION ?= v1.0.0-rc.4 +EDGELET_BINARY_VERSION ?= v1.0.0-rc.5 EDGELET_IMAGE_TAG ?= 1.0.0-rc.3 From e54dff00aa092c3bb8c5b7a7dd2dc7839512dfa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 15:24:07 +0300 Subject: [PATCH 51/63] Apply shared I/O helpers across exec, logs, and websocket paths. --- internal/describe/agent_status_test.go | 6 +++--- internal/exec/agent.go | 5 ++++- internal/exec/microservice.go | 5 ++++- internal/execute/utils.go | 3 +-- internal/logs/agent.go | 5 ++++- internal/logs/microservice.go | 5 ++++- internal/logs/stream.go | 7 +++---- internal/logs/utils.go | 6 +++--- internal/util/terminal/terminal.go | 21 +++++++++---------- internal/util/websocket/client.go | 29 +++++++++++++------------- 10 files changed, 51 insertions(+), 41 deletions(-) diff --git a/internal/describe/agent_status_test.go b/internal/describe/agent_status_test.go index 8a451f752..162208bd7 100644 --- a/internal/describe/agent_status_test.go +++ b/internal/describe/agent_status_test.go @@ -77,10 +77,10 @@ func TestFormatAgentStatusV38FieldsAlwaysPresent(t *testing.T) { func TestFormatAgentStatusUptimeAndTimestamps(t *testing.T) { // 2026-05-07T15:21:10+03:00 in ms since epoch - ts := time.Date(2026, 5, 7, 15, 21, 10, 0, time.FixedZone("TRT", 3*3600)).UnixMilli() + tsMilli := time.Date(2026, 5, 7, 15, 21, 10, 0, time.FixedZone("TRT", 3*3600)).UnixMilli() status := rsc.AgentStatus{ - LastActive: ts, - LastStatusTimeMsUTC: ts, + LastActive: tsMilli, + LastStatusTimeMsUTC: tsMilli, UptimeMs: (5*time.Hour + 3*time.Minute).Milliseconds(), } diff --git a/internal/exec/agent.go b/internal/exec/agent.go index d8f19b80a..31dc38298 100644 --- a/internal/exec/agent.go +++ b/internal/exec/agent.go @@ -1,11 +1,13 @@ package exec import ( + "context" "fmt" "net/http" "strings" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/trust" 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" @@ -67,6 +69,7 @@ func (exe *agentExecutor) Execute() error { // Get controller endpoint controllerURL := clt.GetBaseURL() + tlsCfg := trust.TLSConfigForController(context.Background(), exe.namespace, controllerURL) // Convert http(s):// to ws(s):// wsURL := strings.Replace(controllerURL, "http://", "ws://", 1) wsURL = strings.Replace(wsURL, "https://", "wss://", 1) @@ -77,7 +80,7 @@ func (exe *agentExecutor) Execute() error { headers.Set("Authorization", fmt.Sprintf("Bearer %s", clt.GetAccessToken())) util.SpinHandlePrompt() // Connect to WebSocket - if err := wsClient.Connect(wsURL, headers); err != nil { + if err := wsClient.Connect(wsURL, headers, tlsCfg); err != nil { util.SpinHandlePromptComplete() return util.NewError(fmt.Sprintf("failed to connect to WebSocket: %v", err)) } diff --git a/internal/exec/microservice.go b/internal/exec/microservice.go index 3452ac469..a62221351 100644 --- a/internal/exec/microservice.go +++ b/internal/exec/microservice.go @@ -1,11 +1,13 @@ package exec import ( + "context" "fmt" "net/http" "strings" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/trust" 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" @@ -66,6 +68,7 @@ func (exe *microserviceExecutor) Execute() error { // Get controller endpoint controllerURL := clt.GetBaseURL() + tlsCfg := trust.TLSConfigForController(context.Background(), exe.namespace, controllerURL) // Convert http(s):// to ws(s):// wsURL := strings.Replace(controllerURL, "http://", "ws://", 1) wsURL = strings.Replace(wsURL, "https://", "wss://", 1) @@ -81,7 +84,7 @@ func (exe *microserviceExecutor) Execute() error { headers.Set("Authorization", fmt.Sprintf("Bearer %s", clt.GetAccessToken())) util.SpinHandlePrompt() // Connect to WebSocket - if err := wsClient.Connect(wsURL, headers); err != nil { + if err := wsClient.Connect(wsURL, headers, tlsCfg); err != nil { util.SpinHandlePromptComplete() return util.NewError(fmt.Sprintf("failed to connect to WebSocket: %v", err)) } diff --git a/internal/execute/utils.go b/internal/execute/utils.go index 1374eacfb..27b8c0728 100644 --- a/internal/execute/utils.go +++ b/internal/execute/utils.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "os" "github.com/eclipse-iofog/iofogctl/internal/config" "github.com/eclipse-iofog/iofogctl/pkg/util" @@ -102,7 +101,7 @@ type KindHandlerOpt struct { } func GetExecutorsFromYAML(inputFile, namespace string, kindHandlers map[config.Kind]func(*KindHandlerOpt) (Executor, error), deleteNamespace bool) (executorsMap map[config.Kind][]Executor, err error) { - yamlFile, err := os.ReadFile(inputFile) + yamlFile, err := util.ReadUserFile(inputFile) if err != nil { return } diff --git a/internal/logs/agent.go b/internal/logs/agent.go index d95354987..8d9c100c9 100644 --- a/internal/logs/agent.go +++ b/internal/logs/agent.go @@ -1,6 +1,7 @@ package logs import ( + "context" "fmt" "net/http" "strings" @@ -8,6 +9,7 @@ import ( "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" 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" @@ -86,6 +88,7 @@ func (exe *agentExecutor) Execute() error { // Get controller endpoint controllerURL := clt.GetBaseURL() + tlsCfg := trust.TLSConfigForController(context.Background(), exe.namespace, controllerURL) // Convert http(s):// to ws(s):// wsURL := strings.Replace(controllerURL, "http://", "ws://", 1) wsURL = strings.Replace(wsURL, "https://", "wss://", 1) @@ -104,7 +107,7 @@ func (exe *agentExecutor) Execute() error { headers.Set("Authorization", fmt.Sprintf("Bearer %s", clt.GetAccessToken())) util.SpinHandlePrompt() // Connect to WebSocket - if err := wsClient.Connect(wsURL, headers); err != nil { + if err := wsClient.Connect(wsURL, headers, tlsCfg); err != nil { util.SpinHandlePromptComplete() return util.NewError(fmt.Sprintf("failed to connect to WebSocket: %v", err)) } diff --git a/internal/logs/microservice.go b/internal/logs/microservice.go index e0963336b..cac021794 100644 --- a/internal/logs/microservice.go +++ b/internal/logs/microservice.go @@ -1,6 +1,7 @@ package logs import ( + "context" "fmt" "net/http" "strings" @@ -8,6 +9,7 @@ import ( "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" 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" @@ -75,6 +77,7 @@ func (ms *remoteMicroserviceExecutor) Execute() error { // Get controller endpoint controllerURL := clt.GetBaseURL() + tlsCfg := trust.TLSConfigForController(context.Background(), ms.namespace, controllerURL) // Convert http(s):// to ws(s):// wsURL := strings.Replace(controllerURL, "http://", "ws://", 1) wsURL = strings.Replace(wsURL, "https://", "wss://", 1) @@ -97,7 +100,7 @@ func (ms *remoteMicroserviceExecutor) Execute() error { headers.Set("Authorization", fmt.Sprintf("Bearer %s", clt.GetAccessToken())) util.SpinHandlePrompt() // Connect to WebSocket - if err := wsClient.Connect(wsURL, headers); err != nil { + if err := wsClient.Connect(wsURL, headers, tlsCfg); err != nil { util.SpinHandlePromptComplete() return util.NewError(fmt.Sprintf("failed to connect to WebSocket: %v", err)) } diff --git a/internal/logs/stream.go b/internal/logs/stream.go index 041d58420..9191e7952 100644 --- a/internal/logs/stream.go +++ b/internal/logs/stream.go @@ -3,11 +3,11 @@ package logs import ( "context" "fmt" - "os" "strings" "sync" ws "github.com/eclipse-iofog/iofogctl/internal/util/websocket" + "github.com/eclipse-iofog/iofogctl/pkg/util" ) // LogStream handles streaming logs from WebSocket connection @@ -32,14 +32,13 @@ func NewLogStream(wsClient *ws.Client) *LogStream { func (ls *LogStream) writeToStdout(data []byte) { ls.stdoutMutex.Lock() defer ls.stdoutMutex.Unlock() - os.Stdout.Write(data) - os.Stdout.Sync() + util.WriteStdout(data) } func (ls *LogStream) cleanup() { ls.cleanupOnce.Do(func() { if ls.wsClient != nil { - ls.wsClient.Close() + util.Log(ls.wsClient.Close) } }) } diff --git a/internal/logs/utils.go b/internal/logs/utils.go index 2ed35b08d..c8a6191c7 100644 --- a/internal/logs/utils.go +++ b/internal/logs/utils.go @@ -1,10 +1,10 @@ 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/util/terminal/terminal.go b/internal/util/terminal/terminal.go index 62e880988..0bd8323ee 100644 --- a/internal/util/terminal/terminal.go +++ b/internal/util/terminal/terminal.go @@ -12,6 +12,7 @@ import ( "time" ws "github.com/eclipse-iofog/iofogctl/internal/util/websocket" + "github.com/eclipse-iofog/iofogctl/pkg/util" "golang.org/x/sys/unix" "golang.org/x/term" ) @@ -52,8 +53,7 @@ func NewTerminal(wsClient *ws.Client) *Terminal { func (t *Terminal) writeToStdout(data []byte) { t.stdoutMutex.Lock() defer t.stdoutMutex.Unlock() - os.Stdout.Write(data) - os.Stdout.Sync() + util.WriteStdout(data) } // func (t *Terminal) detectEditorMode(cmd string) { @@ -117,7 +117,7 @@ func (t *Terminal) handleInput(data []byte) bool { case 0x03: // Ctrl+C if time.Since(t.lastCtrlCTime) < time.Second { t.cancel() - t.wsClient.Close() + util.Log(t.wsClient.Close) t.writeToStdout([]byte("\nExiting...\n")) return true } @@ -129,7 +129,7 @@ func (t *Terminal) handleInput(data []byte) bool { case 0x04: // Ctrl+D if len(t.inputBuffer) == 0 { t.cancel() - t.wsClient.Close() + util.Log(t.wsClient.Close) t.writeToStdout([]byte("exit\n")) return true } @@ -146,24 +146,23 @@ func (t *Terminal) redrawInputLine() { defer t.stdoutMutex.Unlock() // Clear the current line and move to start - os.Stdout.Write([]byte("\r\x1b[K")) + util.WriteStdout([]byte("\r\x1b[K")) // Redraw the prompt and input if t.prompt != "" { - os.Stdout.Write([]byte(t.prompt)) + util.WriteStdoutString(t.prompt) } - os.Stdout.Write([]byte(string(t.inputBuffer))) + util.WriteStdout([]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 + util.WriteStdout([]byte("\r")) // Move to start of line first if t.prompt != "" { - os.Stdout.Write([]byte(t.prompt)) // Move past prompt + util.WriteStdoutString(t.prompt) // Move past prompt } if t.cursorPos > 0 { fmt.Fprintf(os.Stdout, "\x1b[%dC", t.cursorPos) // Move to cursor position } - os.Stdout.Sync() } // func (t *Terminal) moveCursor(n int) { @@ -228,7 +227,7 @@ func (t *Terminal) redrawInputLine() { func (t *Terminal) cleanup() { t.cleanupOnce.Do(func() { if t.wsClient != nil { - t.wsClient.Close() + util.Log(t.wsClient.Close) } if t.oldState != nil { _ = term.Restore(int(os.Stdin.Fd()), t.oldState) diff --git a/internal/util/websocket/client.go b/internal/util/websocket/client.go index 115fe7cef..4aa8d3f27 100644 --- a/internal/util/websocket/client.go +++ b/internal/util/websocket/client.go @@ -37,16 +37,17 @@ func NewClient(microserviceUUID string) *Client { } } -// Connect establishes a WebSocket connection to the server -func (c *Client) Connect(url string, headers http.Header) error { +// Connect establishes a WebSocket connection to the server. +// tlsCfg is required for wss:// URLs; pass nil for ws://. +func (c *Client) Connect(url string, headers http.Header, tlsCfg *tls.Config) error { dialer := websocket.Dialer{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, HandshakeTimeout: 45 * time.Second, ReadBufferSize: 1024, WriteBufferSize: 1024, } + if tlsCfg != nil { + dialer.TLSClientConfig = tlsCfg + } conn, resp, err := dialer.Dial(url, headers) if err != nil { @@ -57,7 +58,7 @@ func (c *Client) Connect(url string, headers http.Header) error { 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() + util.Log(c.Close) return c.err } } @@ -66,13 +67,13 @@ func (c *Client) Connect(url string, headers http.Header) error { 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() + util.Log(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() + util.Log(c.Close) return c.err } @@ -156,7 +157,7 @@ func (c *Client) checkPongTimeout() { c.errMutex.Lock() c.err = util.NewError("pong timeout - server not responding") c.errMutex.Unlock() - c.Close() + util.Log(c.Close) } } @@ -165,7 +166,7 @@ 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() + util.Log(c.Close) return } @@ -173,7 +174,7 @@ func (c *Client) handlePingError(err error) { c.errMutex.Lock() c.err = util.NewError(fmt.Sprintf("ping failed: %v", err)) c.errMutex.Unlock() - c.Close() + util.Log(c.Close) } // SendMessage sends a message to the server @@ -205,7 +206,7 @@ func (c *Client) ReadMessage() (*Message, error) { // Check if this is a normal closure if c.IsNormalClosure(err) { // Normal closure - don't treat as error, just close gracefully - c.Close() + util.Log(c.Close) return nil, nil // Return nil to indicate normal termination } @@ -223,7 +224,7 @@ func (c *Client) ReadMessage() (*Message, error) { c.errMutex.Lock() c.err = err c.errMutex.Unlock() - c.Close() + util.Log(c.Close) return nil, err } @@ -237,7 +238,7 @@ func (c *Client) ReadMessage() (*Message, error) { c.errMutex.Lock() c.err = err c.errMutex.Unlock() - c.Close() + util.Log(c.Close) return nil, err } From 922ecb5bc32e8df39a56fe22b1ddcb768555ec0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 15:24:13 +0300 Subject: [PATCH 52/63] Replace legacy GoReleaser configs with unified flavor-aware release workflow. Drop packagecloud publish scripts, add GitHub Actions release job, and align govulncheck and gosec with dual-flavor build tags. --- .github/workflows/govulncheck.yml | 23 ++- .github/workflows/release.yml | 55 +++++++ .goreleaser-iofogctl-dev.yml | 199 ---------------------- .goreleaser-iofogctl.yml | 263 ------------------------------ .goreleaser.yml | 220 +++++++++++++++++++++++++ .packagecloud-publish.sh | 107 ------------ Makefile | 4 +- goreleaser.yml | 0 script/bootstrap.sh | 25 +-- script/bump.sh | 35 ---- script/goreleaser-env-check.sh | 7 + script/goreleaser-env.sh | 72 ++++++++ script/goreleaser-release.sh | 9 + scripts/vulncheck.sh | 11 +- 14 files changed, 393 insertions(+), 637 deletions(-) create mode 100644 .github/workflows/release.yml delete mode 100644 .goreleaser-iofogctl-dev.yml delete mode 100644 .goreleaser-iofogctl.yml create mode 100644 .goreleaser.yml delete mode 100755 .packagecloud-publish.sh delete mode 100644 goreleaser.yml delete mode 100755 script/bump.sh create mode 100755 script/goreleaser-env-check.sh create mode 100755 script/goreleaser-env.sh create mode 100755 script/goreleaser-release.sh diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 31c933490..1275eabed 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -4,10 +4,9 @@ on: push: paths: - go.sum - - go.mod schedule: - - cron: '0 6 * * *' - workflow_dispatch: + - cron: "0 0 * * *" + workflow_dispatch: {} permissions: read-all @@ -19,10 +18,22 @@ jobs: name: govulncheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - 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: make vulncheck + 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..55e296f74 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,55 @@ +name: Release + +on: + push: + tags: ['v*'] + workflow_dispatch: {} + +env: + GO_VERSION: '1.26.4' + +jobs: + release-matrix: + runs-on: ubuntu-latest + outputs: + flavors: ${{ steps.set.outputs.flavors }} + steps: + - id: set + run: | + if [ "${{ github.repository }}" = "Datasance/potctl" ]; then + echo 'flavors=["iofog","datasance"]' >> "$GITHUB_OUTPUT" + else + echo 'flavors=["iofog"]' >> "$GITHUB_OUTPUT" + fi + + release: + needs: release-matrix + strategy: + matrix: + flavor: ${{ fromJson(needs.release-matrix.outputs.flavors) }} + name: Release (${{ matrix.flavor }}) + 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: + FLAVOR: ${{ matrix.flavor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + run: script/goreleaser-release.sh release --clean 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..5df6907bc --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,220 @@ +# Unified goreleaser config for potctl (datasance) and iofogctl (iofog). +# Set FLAVOR and load version pins before running: +# FLAVOR=iofog script/goreleaser-release.sh release --clean +# FLAVOR=datasance 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/Makefile b/Makefile index 13c08bfd8..17d15ee0d 100644 --- a/Makefile +++ b/Makefile @@ -148,7 +148,7 @@ vulncheck: ## Run govulncheck on module paths go install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION); \ fi @chmod +x scripts/vulncheck.sh - @scripts/vulncheck.sh + @GOTAGS="$(GOTAGS)" scripts/vulncheck.sh @go mod verify .PHONY: security-code @@ -156,7 +156,7 @@ 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 $(GOSEC_SCOPE) + @gosec -exclude-dir=build $(GOSEC_SCOPE) .PHONY: test test: ## Run unit tests diff --git a/goreleaser.yml b/goreleaser.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/script/bootstrap.sh b/script/bootstrap.sh index 1068c2de2..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 @@ -68,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 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.12.2 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 - fi - ## TODO: gcloud -fi diff --git a/script/bump.sh b/script/bump.sh deleted file mode 100755 index 04300b4ae..000000000 --- a/script/bump.sh +++ /dev/null @@ -1,35 +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\/v3@).*/\1v$version/g" Makefile -sed -i.bkp -E "s/(.*iofog-operator\/v3@).*/\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 - -# Pull modules -make modules diff --git a/script/goreleaser-env-check.sh b/script/goreleaser-env-check.sh new file mode 100755 index 000000000..9ffe49ba8 --- /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 \"\$(FLAVOR=${FLAVOR:-iofog} script/goreleaser-env.sh)\"" >&2 + exit 1 +fi diff --git a/script/goreleaser-env.sh b/script/goreleaser-env.sh new file mode 100755 index 000000000..c1f5f602d --- /dev/null +++ b/script/goreleaser-env.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Export version pins (versions.mk) and flavor ldflags for goreleaser. +# Usage: +# eval "$(FLAVOR=iofog script/goreleaser-env.sh)" +# FLAVOR=datasance script/goreleaser-release.sh release --clean +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +FLAVOR="${FLAVOR:-iofog}" + +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..3f0ee9f36 --- /dev/null +++ b/script/goreleaser-release.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +FLAVOR="${FLAVOR:-iofog}" +eval "$("$ROOT/script/goreleaser-env.sh")" +exec goreleaser "$@" diff --git a/scripts/vulncheck.sh b/scripts/vulncheck.sh index fcc5ee798..cd76bc82b 100755 --- a/scripts/vulncheck.sh +++ b/scripts/vulncheck.sh @@ -1,16 +1,21 @@ #!/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 -format=text ./cmd/... ./internal/... ./pkg/... >"$out" 2>&1 +govulncheck -tags="${GOTAGS}" -format=text ./cmd/... ./internal/... ./pkg/... >"$out" 2>&1 status=$? set -e @@ -53,9 +58,9 @@ while IFS= read -r id; do done <<<"$found" if [[ -n "${unexpected// /}" ]]; then - echo "govulncheck: unexpected vulnerabilities:${unexpected}" >&2 + echo "govulncheck: unexpected vulnerabilities (not in SECURITY.md exceptions):${unexpected}" >&2 exit 3 fi -echo "govulncheck: only documented exceptions remain" +echo "govulncheck: only documented exceptions remain; see SECURITY.md" exit 0 From ca2d56e386a160a4b86c712ad68b487b178c57c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 15:24:20 +0300 Subject: [PATCH 53/63] Update golangci-lint config for v2 and moby import grouping. --- .golangci.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 1cbc7a1a7..66dbb7b8f 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -18,7 +18,6 @@ formatters: exclusions: generated: lax paths: - - ^\.cursor/ - ^vendor/ linters: @@ -119,7 +118,7 @@ linters: - github.com/opencontainers/go-digest - github.com/gorilla/websocket - github.com/vmihailenco/msgpack - - github.com/docker + - github.com/moby/moby - k8s.io - sigs.k8s.io/controller-runtime - github.com/briandowns/spinner @@ -131,7 +130,6 @@ linters: exclusions: generated: lax paths: - - ^\.cursor/ - ^vendor/ presets: - comments From 84723853a83da96024532ac05d805debbdd652b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 25 Jun 2026 15:24:26 +0300 Subject: [PATCH 54/63] Document v3.8.0-rc.1 release notes and security reporting policy. Refresh README and CHANGELOG for the greenfield v3.8 line and remove the retired logo asset. --- CHANGELOG.md | 59 ++++++++++++- CONTRIBUTING | 38 ++++++++- README.md | 208 +++++++++++++++++----------------------------- SECURITY.md | 58 +++++++++++++ iofogctl-logo.png | Bin 38107 -> 0 bytes 5 files changed, 230 insertions(+), 133 deletions(-) create mode 100644 SECURITY.md delete mode 100644 iofogctl-logo.png diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2356dbd..16ed7ace2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,63 @@ # Changelog +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). + ## [Unreleased] +## [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` with `FLAVOR=datasance|iofog` matrix +- 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 + +### 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 + +### 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`) + +### 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.5` | +| edgelet image | `ghcr.io//edgelet:1.0.0-rc.3` | + +## Pre-3.8 history + ## [v3.0.1] - 27 May 2022 * Updated openjdk-11 installation on Ubuntu @@ -199,7 +255,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 e2ca5f038..ad6b12011 100644 --- a/CONTRIBUTING +++ b/CONTRIBUTING @@ -32,7 +32,25 @@ 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 @@ -43,6 +61,24 @@ CI runs on **GitHub Actions** (`.github/workflows/`). Azure Pipelines and the le Local checks: `make lint`, `make test-unit`, `make grep-gates`, `make security-code`, `make vulncheck` +## Release builds + +GoReleaser uses a single root `.goreleaser.yml` driven by `FLAVOR`: + +```bash +FLAVOR=iofog script/goreleaser-release.sh release --clean +FLAVOR=datasance script/goreleaser-release.sh release --clean +``` + +Version pins come from `versions.mk`; flavor-specific ldflags are exported by `script/goreleaser-env.sh` before goreleaser runs. Local snapshot (requires Linux cross-compilers for the full matrix): + +```bash +FLAVOR=iofog script/goreleaser-release.sh release --snapshot --clean +FLAVOR=datasance script/goreleaser-release.sh release --snapshot --clean +``` + +Validate config only: `FLAVOR=iofog script/goreleaser-release.sh check` + ## Eclipse Contributor Agreement Before your contribution can be accepted by the project team contributors must diff --git a/README.md b/README.md index 2a30f9e1a..b4b010784 100644 --- a/README.md +++ b/README.md @@ -1,177 +1,123 @@ -![iofogctl-logo](iofogctl-logo.png?raw=true "iofogctl logo") - -[![CLI CI](https://github.com/Datasance/potctl/actions/workflows/ci.yml/badge.svg)](https://github.com/Datasance/potctl/actions/workflows/ci.yml) -[![Release](https://img.shields.io/github/v/release/Datasance/potctl?include_prereleases)](https://github.com/Datasance/potctl/releases) -[![Go](https://img.shields.io/badge/Go-1.26.4-blue)](https://go.dev/) +[![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/Datasance/potctl/actions/workflows/govulncheck.yml/badge.svg)](https://github.com/Datasance/potctl/actions/workflows/govulncheck.yml) -[![CodeQL](https://github.com/Datasance/potctl/actions/workflows/codeql.yml/badge.svg)](https://github.com/Datasance/potctl/actions/workflows/codeql.yml) +[![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) -Canonical development: [Datasance/potctl](https://github.com/Datasance/potctl) · Upstream mirror: [eclipse-iofog/iofogctl](https://github.com/eclipse-iofog/iofogctl) +[![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) -`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. +[![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) -## Install +Upstream: [eclipse-iofog/iofogctl](https://github.com/eclipse-iofog/iofogctl) · Development mirror: [Datasance/potctl](https://github.com/Datasance/potctl) -#### Mac +**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. -Mac users can use Homebrew: +Release binaries ship for **linux** (amd64, arm64, armv6, armv7), **macOS** (amd64, arm64), and **Windows** (amd64). Built with **Go 1.26.4** (see `go.mod`). -```bash -brew tap eclipse-iofog/iofogctl -brew install iofogctl -``` +## Install — iofogctl -#### Linux +Package repository: [iofog.datasance.com](https://iofog.datasance.com/) -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 -``` +**Linux (DEB or RPM):** -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 +```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 - 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 - 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.5/install.sh -o install.sh +chmod +x install.sh +sudo ./install.sh --version=v1.0.0-rc.5 ``` -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/iofogctl-logo.png b/iofogctl-logo.png deleted file mode 100644 index 9485e1eeb6a3d2ce43a93ab58fd681ea6da3b275..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38107 zcmZU)1zeQR^Egf@h?IhG^npsKbmsw5B1kBLba!{(i69};N=O`tB1(62bax$%38G*rlMGu+0*!y{LHruY&Mk3bz)F26;D z`z^JM55>c~-Dj_$prxvyz^dipYGdzYjfeLv>T?RQ&Z{k|Zp7ko{0PC_c&Y&EZ_iSw z0`P8bu|8%ax$``dM835+@=5LE$VfB&*4KA$!U=i|%FL9?%$HcBKh*QmI^{Q#O1JF= zBAR>`uQT?8VCoy`Y5Q4Hh=H5gNp%+Pw7+i0^YndIBtn<3P*drDW6>gbVMahvSu6cB zmHF;nqSUTa7_yyNF8nNAw7m>_b$$I~mbS5}j|_h!p($4(c@ zqq98--;$g}m$i#BLc;meWrT!v*x~tI=;kxok5za*!n}HkL~=r{JB89F473p&ab=m2 zeD};hUJ>z`NT$RIBm+3CZ4qP@OqS7~U>&NLh6DGM)Op2TwC4KK-<>h#qOo;HT@QC3 z!airRGxnQ@^BB}W6sMXitB-F;lgPPE`(2$9XvqfYfP{@(w+dJeBwet_3P6#-U*hQ^ zGEN^~y$@kh$}rM6rX&4^uWiZQ8>O@M{PTHPcj^=R;V1ncwWr5#GAUu+9dFhO3{iJ! z04?opbT(__pl!SfN{_bzk?(@KQACgTvUslggWofC&deWgnsqMJt)7r7?i{9$`6)RE zg+N+rH9xkQW*`fC*oQu7os*WoyXRwX@@d71_w;EjG)7Choy_G~2b}`l4SZ6IQW=K( zA>{5Y_>fyYGy_1eN-LjA2H)Mh?}+m0Hz_1D?@P%(WZ#%-SI&R8t3?`UCd(9NBTn>VS=uNZ z>Bh+NmMLJF{kWZ&PVr7y*vMU?uQ7D2d66P3Z)@+HJj0_3SG-U1OfWG#@(z9Z9sQ6` ztk%zH$_alefaPQF|44{_#_2)1rF;=z(*w8{F{VYXLn)%bnOn@oNUoGSs&B_23E+_< z9j&rsHwfKRNFN#Bv_2MntYZ3=P`u^Qa;msO2Kf*TB7(G=edVP))x3p~Krw+{sAEgj z%B;JXyT|;Kd0Xkb!z~pDqJES?{2-#?_Tf&xu98;0<+I=E2eyY|hYROmj4wfEwpuAi zEwy-<-%|#2mYhf}`DrEbr{XHLFE-hOSgE4azekXNAXb9h09gbp~~=bn1RZ zzb<$k_si!eMZUnq&ScwU`Q%jo?(2s9Q=`mpE_p23LwbBVC9h?37u#C|KGK89SS)W^ za;`Gv>V@W;7q^b~jBt%kmyG6&mi#Q1P!iJhY__S|aDO2|-7ReCk*Yu7JrFt|HNZ!G z%u&jDi=!}Wgnzj!EY5(sDyZ^?L8n2CK}}V&T}35frBIbxReBY0>$i8Im(AwD_)MOs z24Ye%CW?jHpE{b8CBx4YFj+MgGhv)E2nA7_e`^}Mt}d~;*;RK<~VRTxDHEl z1tJU&NeLOrp8x%=l{A?nda%#pb@@>F1-UnQD7kgG6qytmam+xB#m8hu5e6B-Jcb5_ z9}FpsHS}lcbgAk>`gSi}J(fOMY&(2$sqd{^IL=M0)6bZBTwzmz*f7{&LJ^<>P!Y6V zq^{9*v?8=M+)>HJ$!W=kJSB!Xx*ED$x|6!_LTyJ|$D9*g^z{#LwzdYacc^xo>$)y2ZE| zuLiWn{N6xTckHIl+4|a^8J&ghK?0`ztO9mU{6LdIzs$CWe#&GiXBiDMXL;}?>y^Vb zO*~u`T#J2U&ZvQwz~*6IsI8Nl4^bfSvZ^Grq~k1dUq#jzrUd?hamUJIJF(38+xP@?)7F8e*wSh z8>Fz8K~E+5IE#i@ChwO;CW_^Z1M=m#`vVU)lrfT_;twp8Pd@%gI87jZFqccG?fLao zIXLn(#sqN1+I3%(bHlB=-SZDRDT^m9TWn{eB`A*Q|$ z{SE^hww$&=nxD~vO17TIo*M|S4@I3y7HFfhI!45)+Eez1Z;@WfmF#+ijIqT%+*VJl z7yM29C;g!tokLQ>rq0cF*v~-)m`U{TZ30rT$y?JF!;$I{x8?Hp3(?cjKHSQ*Ke$~C zEgHYMsqT+ue-BL7PO;`~sGoQ7pE){vvZ7l_yG_H(v!hR6S>F7t?%B(Usy@rfQ{aK@ zEy(7Z-P!=u_I@X3MUCADCk<2h!IaU(D7L!~7%`Psy!!^x=rR|u5-@rp^?mVsVBbXF z?Y;m}^|=w9p~rl@5SaT=S2WG5$JMT1T@sxW^}=6QJBiLTf>H2a$G_$#n< zkKCU?kTK4nIfiPaGv5q$opV{!*Z0P*-7kTHpZV-l8eP?1!;W}e3^J^ytEI4eQ(V(S9wrTwuZ+P&+K=ocEsmP5zgWy zwHA=ST)MT)qe7?hAw511lZV|nw((bcs)iVi&vhk$;)W_ zS!`Oh_J+J#c~zqEBXiF_ef;#dIwsA~);42T91yVAhHgRG&d%&|^7)yF`;1<#jH(r< zeBqNab*W$RTJL%Nqd-M6L+VR$osnbRo=;PirvoC2aZyZT=@{mZrA17w&yBnKE=JXP z);#&C22O#Tp_>W=UH1?Rz@XA&wq^6f2a_r^ao#{U#s+PeJFgFJoP%BVCFOHc2KdSF zK~k{Ts+xmGSN5Gkp|;*QYXl-mcN(x38_|m?D=DERAFx`Nmi>;=@QCYUH0n7d2(r)E zo1RnSxM&ZXy8`!JR)<7}bOXy}$-vy$Z&&uiw(#Ly)0Jjv#*DMi$5lt^>orsRIZHqa zYfAE{I;y=MuD9*9U%uQv@Dan4!GG({{8rM_E05)_-~}`P(*vlV*B(oPkeo9>?wH_h zX#rk(L1_YQbV=fkJ2~%WWbnMbbP6HQzie;vGemF1#BjughcD{m#XrYy{!BqmK1D$u zT+sO;7|-o7#48z?3_3MVIHS20-BYtj!~GsIDvM{%b1z@~`T~q|Za-MRQMGyT0`D=d zd<*XeJ_FuOTnQhSK=_RRE-T}6;Sv6QPk@IPVUKs?pEeq}{Ld4QOMhVgl@lh0;}PTj zQQ?wbHo^b3CQ#2N{J%0@Ij#;*{*{8NDlUIz>0xc{;%VpV<;sozgOkMVnZ73;9xeNy zgs=MY;Q~tvxM0 z?A^TVU0qoJ;5D~!_4bmvfBz4me?R}~r?sE`e<-s zR#MB}&)VsYqP;UtW;hxk5pi+pzwQ5@n*UJz7vyVCYYzoiXB?mx=s!aL2l&4$|Nnr0 zN$UTHq=4A}ko>Qje;}p#{&4R1Gs4>ta9sn_qhD0kG+lw>sGA)ty8Y@bZ`s^U<@{AXJC)O=BkTiaDM(P`FOh27P1j#B0%sY_d_c&k(*`n z^keBy3IKKm#oh7b_v9PO1pFC?BJRue72e8t7!|_CujSfq1q+Zn==?4lmYUfA51P%$ z5EI6@qcS(elh@;Uzi-_1chT8nnMt9mk5*!Q2%hO(OeKwJ2dh}Irz(93=_99hP4Kh# zSU!uxG5AM^qC)OkWp}eEQf~RU(h3e1PPTuINMsq?Wmz24vdH!(a&S()@!Chjt3_sP zN7`;m~A#9 zJZhi&kkQ3xZB?z#cE-?O@_)POO%&jg>U4zZc#XwSE!qE4P`OZl#hjd|@sEA{K}8+$ zN0~Ibnt5vSAz=GDnnkn4ia`~|;k^a1>lW#wxh3IhHKJX?k$ue7b{6!Dy<-VnJhgZi z9Dq96h=W~uuZkzh2CAm>P?c6(>nochl$JfDol2Jb-Q^} z6~UY`8tIw3Vyip>l7qnoF3kdNfaSt;BgH zYLHRaWK#NS4LaC5UL#yG1YKg88ybIEl4cRx{QGhHNjt20iK%8Tw#f2fM3++olEtP^ z@YP_WthAL&?LkIDuGIvJv5rKIgqMLx!`Y0#?ukEQ(fa<5@>|U2t5-xTR^YonDcyyg=a$r(*%(BRIiGW zB?7iU-}7gq{M+c539Ii`RD`krqOmttP-gD2-#Xdcu5)UBAn4yfLcZb*do{#gpkdhG zh#8CC_`O>#JPf*;VBHItEk2Od6O734F4K1^>m1;Ncr>I1kC@1ujGGA&`BiiPqHk9A z%LX8}Hbx^@>^96Y9EVfA1=nk^JsV;}Tbj%4L>CrjzV*C^`!A2Yx47zg2N{pN?YrE) zn2mlkAkE?j&$esM zB06p5iR!(gq=|H8DK9~H?$(e~yCfVojkB2wIKnQ26Zg5m`QQnZp%tyebFT!8Y$Qr_ zxeC^ZTN;CqZXdC^zH2)}_KLE{SIfp{!uyv!K5Yl)(2Gd_70tNHVV~cJz^S10nX|QV zr9Ron^H9_3O`>{` zE42xf`n9=kllePBh;Q$H2LD(#OGh=nI`CIR@CnkU$XiSpg@9R{&hIQ7i|Su4#gevv z5%kvgoVSKYpZ}0;9Lhk`1lBy7P`kPyQ(wAdMGXzsP!VE{=pZ1cPks3avFDzjJyglw z{#6*_jkzM6#7?io172x>jt5c`dOA zbEavQve!S0%~YstXVz21R5C7QgzS&3P(O&lwy`tTmkE8IGwY_BXk&r$|5Z-0#_xJf z=J%ZzISxy{x;>c_+-gv&9}F;uTCdh{gX|O0UEZfrbFg}Zi|0{ugE#F?$dWX?@L01T z4Uh#H+Nt<{HUGl)_XDEJlOa>#^dKkV-VggzhsF)sk8`t@xCUSMu?yh+6(CEhE3CN8 z{3}l)ChopF|~AA%m!?r%=FyLHiyZ5Z`}qh*4&F(KlDF(Q8)Bf zh0Jd2QHh93Cgi&`*>SIb+?RF`dRb#pclSe=_HfA)7HSQPY_-2c7Ts)Xy25)G%stMM zF)C?99~?2x^{3F_JbmjEk-;Ts@KlgJ@{ID6v9To>x%sWYHHngI>3Uph;3x5(K3sU~ zCYe+H!jHjZ=vCu0KFM`IPb3Y1n1Q-MY8(AxXGV3DlW2v^D|nHk@m#u+y{N`K(fDO; zFUe)oQ-`l}{lf=cpt_|Tn^4G{5#H(C>C%Abu(XUcKLQ<7fC&xCjc$-_Tx-}UuQLTo zZg3H(e`yR`9?pJYyytyXs*P1mPcaR0w|dEZf>E~l`tp7hey1E9;oSCWjq;~{o%zZz z_n}V&sjP*W4k_gH{nF*S2WIa$wDwvA1MHZTJr3MER7sO6Zg9}qVs5F7v~4-uY|>O`wqReK$VS;ntl$)1MgTXHk`TBEJIIl| zDk5$G;HS{$jXlGl#91^0LoQffaGl z^B$ML^fMPrvF8bHQjMY!kI}(2&^Dwwl`z5UQ=kk z0XzC2;T7RLXbSan!Vt}=SmkD!0zW-W>S)0&vwtQe?}~>GE~z_W>*c`3_1_mw6*r_N z79JW9TfY}8ak9w*hb%Y9o-GCckT`iOvz>bKz2bVOV$;xnd_aw_k(KXw7<1CwjN$H6 zb}Xt%nAv@D8atK=H+3G8*o#c~YKdFBe)c}^eMrRgXJ*AUVKXM)1%%hvQusiZ&g+Ks zTvDybM*`h3(H=f=_ZBUGRUH1z6#@;_T-VHjHe}|4bU?p{T>E8Cd)kYLPvcy#WNn}& z5JOB}gWA=21A@eJz~Q28&^hCBVvKpMMOBo-J5I@rEq>iw=lp)ZH6Y>n=BtgAZU=Rbu-4 zS3v&spdez@3Timrb?2;etfb{E`V0wOa-QOqZXQZA2X0XgOi!o& zSqai`OTzx2rO`)*asXkqKNPEtOE&VJ^xMLGuCi)m_&_wF zS4PB_GC+>!V46?l!bX`- zPQ{#EJlyn1((^@wDN`!fWd6&~REqxN+h3M_FVo!@H23@@`XYMDLv7~YL4uiupjKe} z1ZK`X^v+kYm(xb%D}4W{{S)bILjUavTOlnVTE!5>>#&3Ayjz01Add0XpPBzStOIzm zDLXH&ksQAeJ*i{snstJHkM-9~ZU&PH1UUW} z{mVZ8b29GhF0r(_H{rh}2`fIl(0}}yk;A8NzpeL_{fs*=kCCBC;9b$L&YH0VwvXK}ks`@knufN) z6_@Uu(ZBz3Km9~EmsoVZ=Bvl$GI#IC5F1Vi00roey^ZxW=fZYV<5IaRSHUK(V{7aK z2}@U5Y4!8SfeKAoYl^I9>n+V@tHmQwp)}tUgiuKe+G*_Z1y?ue6|J*E%r-VPt$HY+ z^}(PU*>i;WD?LM0YwZ*SKV&^SelcZK{`_U4aKw)RX;pn#|jb*~e;t#=kt-hBH!X z?%Z>jTC3)5dB+r76k2_%AUK(cbI&WfW|7($(t%MaO|dx8G2X{_2K4L7eQOfzOF!cf zFN|`@eyFp|QqJlt@9q8N=n~fsxtk6fYd^s6LEv@?#qwu zSDikepS%Y+Y&)Sxe9yZiag@1$@J%+H|Q1fJtfKo z?SW{#0*EX6wjIA**+L&>?KQ2c*zBoeL#9$+(CJMB0NZcrP|d#%`!L~0r6t+l>ANYb zJ^<1MVL8$}5m~F}YMTB<65E(3gSgP@zAQ`NCIv8W$uCfeJ_Sv_UTwODQXA4^GR1!+ zBcsAb!TswXJEEQ$vxwfR4T-L24)QC2X_$>tfZdy%kYMVOA>xq_-b}C_TfgQ%;O4R) zS!00?lL(Ox(#sq~zs%W+TOV);GWXYD9B7;)RJxCJ^aqPYjC$WYy5}NL5FhLOI7W zj;qN7Yz9MTgi^)VvbOa#-s{GH!^D^9zB5%fDct*B(p|W#5fRi`c(r%d+d>k!XMp9mRPoXr%6GzO|j+YBz!sgfx=}q|W9=!o~;Of{y~^M?pjht(td% zvcHT6mk$nV7X53?ntoTfMlwztoh5f7C`DI=u6O2JPAJj$U5*P-!@^ZOvxBJp3rzpY zzL=h$+|aBh3UgJ(;IeQLT_RMX7_+?M_)VHtnW&v_So1Q*ywj?5`HR}&312L+l%sXr zRgc-LSkpl^3%Y_u{<2@VSa)x4x@O(X=!bHtb$!4*`!}UoZeL$j7@h)&pkYbvSl^NJ zUF@Bs{ryW#&y}nOIqgY?fvPLzp%;bRV8O%lKBF(9d*WHAz*%?GILdG+?4XTro9@)@ zAjKH$@7YnOm#Uh?nJz1H9_UHdL~dZ>6@}TI8u+< zk)7z0)N5%L%vav~fJ2b2=cebCNN3I6t#hLWOAICeohf0HqB*UDvGO$1d3?@S2#{Fv z22Z`f=63@$u)O*j@z_y@z0_+#3;|RDblqi$^*8YguyfhGKKJi0a41C%`}M+vVINQ> zOPFZn$Kp`+?=o70h?{!JkhxJ6vBkUBpM6tn);rZx`^Qa`g8TD9Pvo;u>rI2fz6&mg{P==nEVx-=sbXGSST%|g zEgtb1_Kny@stekD7Ls*YN4#&j*A!iAmkRkbOoUnzhA>wc^GQQo7PTDZJ1+{_;#SaA zY*yqnrYnpqXuZ!c9GqUM$sVSRnP@*h@BnDcM{0j{>9}#DEAD6}^t#PE#@3Ah!AE)r zNg=Whlyzm-7bqjnfLTSVf!mR$^v%BFzfZcmqa-{+lu@tHH8G|}wtks;w?d)~NW(wD z5bqgVlwQ2+BTq^Xn1zlobFjhHdLGQu=ACL2$lWu7KMLzSH(JvL?1&RNkcMy@nk5@! zPiBlMF0ykmlYKrfrzu732<=u8^%{@5<5pUmWG&y0g21V;U22wB6?YEQuAkq%{R=g` z;O+Nz@8xcJ(_rofMr=?v4p0mrseg?dPeYd_(jiqNmaGp#4_<)7z=x}-Si4#1^`c*k(gFy4 zSatVJ+3X6={hj`#v&)i;4)nNd)g*tIm6_GhhmK|z`L>`&*_R(P3eA;37DS{$=X;>J z?yF#x!T2W3k@!rcK3|hSTR$j5yM>TR8}~aNEWL>m62rPoyB!1@+=@dQeTb|v#Jtu`-+<-# z0h>AnBNo3@umdtkr^vOdjz2xZru%3P1%ATq)7p2VBHjh%Q|i$@C9j(pi9^7SKUu#+ zw+EPCTRpJZxpfnpPsv1<6E6J@+P#Vatu)gUS?(+>jO^N35FQgTn1sJ0ni}R6%6`fJt~&L?&K3j0GS}B2-i|rB$S0Pufgq`hsKc`Py2v5 zddqsQslR)dj0qU&>|RDxZ@%JrAws`0brKj;mc_8-)u4&C#W(iaKHQk<=c`^Ve3-;X z45*|tAnuD`{Pw(?P)c{rqT9pocqhkjeMZ3`Lg%|0vHLQnRDFp#q4}hD(Rno3);4pL zzl+tD)P2QaFTzqF4JC8~LtmkBI@}^@gV-H}^b3_5sY^ndkq>fs_|si#9t_q#g})aFO026a_cwn{^?`Jn zu41cu@}eU^K4?1QmMCzxg>#2&u~QXnj3sz~_&kW~*K}>K1;y(R(P?vuvgF}5g-oqb zdCJsIq4Avnw2V~QDSO#Ef=~RKVdZi#XRVbuC3-F5MgrLpnMVkDy2{29#FHZtA_6LX z?etE8I1Em=)XB$tDRa$L{4$84|NZX0V5~nzF72zwX&oF$G8HROdiH%?THTVYdzI@T z*O}j_yh}W6)Bj1->*a=X8ITx3I>Y&Q$Iyi`8N?$cs_H=^r0G!Z@NdZ;#N8$rgQE-x z-np6mJia5ZdHdbt-;nVwuT0{_pA;p;*Vx9p>OQg3Klcb_E8I_4s6ch?ohR%sT&Q&> zEpHZ*KN^p2kd9Ct_`>8-u|+1bH4ETq>BX2P(gIOQ4R@X-a`FhwF!^VJXODHa?Z-uz zKXP@krcr8kuB8_*16Wo{Paa-085nz(=u7tLqaj!7~3B6wR z#&W*64gMa23QQ<=DcmdQJ3>ved%V0LFzs0<@8ydFe+_)n^m4_5#W-g?C3c*ZAlQG zeZJ zpWPQAWM05z4YEoZ{ALiknp8!K=|q?^iSj$jS zAhP5&l3lZiTVKBt9)>wxA>j1Joad6m-+aPMJM6`-G2W`izOBA&!>tFoe&9vCU)#xt zMYbt~W)3UxY5oLDG*CO2=w;Fu=el@>y77?g(I0F;@wl9p*b8mHi;PK&=yZkQ#_7U? zJh61ZHP-inr=n;8H4I8J)@n38U3QyXc(e~ko^%0o5mGb7PHh7ino+D zPP<3T8Wc$v&)N~4k{U2VjQ3`14m8uBOvT2f8S~V$_a~>)VK*%&Xm|uw^W8rS{i#j0 z?kg%(-4t`;SDcA3o$99tj0WiU7N_jmH%Ygnh-L0{vxct5@qm=k#)(Cmt$~uLm^Tq& z(Dlyvg+a*pNzKqTIlMoLUS*=~LIVLz3R&4mYIvjaUUW(Xc@F_YqBIG1e_xDyU0LI{ zk`%QskU?&l@!Cv8h^1k}ZUki6J86$s=6Ya>OCTLK=yrev`c(z}Lh`~iknq7+R1GJY zbrdR(0EXLac!_|vee>@Kus6Eg?0G5m&|iG*(nZ_ByV~1Tpy+b(#Xzhn=;i&otufT# zch6=MW|~XdAcvNc?*8z?=Pa;$l&6!MF2zUS%fY4g7$Jr-tc> ztmPO2n%K!h*k0f}bh;V}ksoz9ocuLDFgig|1XYqDdw%H6uStHG>-QMan-Clz?NE~` z8$=l4c&}^azQ9agx3BR}FZIF^&g+#YiPzH>l_qwpJMex^yC&%u$WB<#oY~i)qxhlG zU&+7+=Zh@hpj|3r&6TPv41g94M?go!@Xz^-Z663;PogxU`o>&suK?#6mm&n&@86>O zpsqNdh63c@{~U-cSq$&JW`A2qN1bMUzRpt=U9DK4&cqfk_u^ui?gZ^Z?v)QR8~VF9 z>9PS^S-kLnw|KGao<&kup&-J$S4v3hzyo(~u*g;d)81K>VE5A)jF4u+@L%(H6v7S; zm+H`>Hn?NBTLOB}D`(F^35>42GG`4%Y9Q@low6tdl7~h~&d2vwDxW{W^Ze#mVD8-c zFblF3fX@;10oHm*iaxv`i|(4(brLL6%Ek05dVD1;f!tf`4K~|i2r#-tEs<2QIs@zvVelOt(lXqAE4{5SN9HVex49a(;mVNTyrrO8S7L1Ehy7a zhzw6Gs8s*US1zt~6VUGg6j4Zlf+4M5Aq2~hy^Gj7DHkaMT=0VcZBav}#uy`M-tLTn zHZsqv*Xyni+{)ZJKWd(}yN#ZSpbrr+Y9b8a{1cM%)}+}7zB=qkYQ98W_XIgZK?~m+ z=f+B^0VyTSZIJ~rWJvMT1)FdnBW3;Q0+U(xyKLle{sz=}?IJ5JI#9B(%H^QG7cSqi z#+Y#9qLmZY>7943sHMEt>ar#$^_9jz?%!u&E(~0NtfLe?A3{gcu<`q~b%ViZ}!}-3M=io%y7ax8x~a z9BU_GOl2=fD#)+mg;Jtz#{=pF$Xp-WkhtFmM}3lXF1+S;Y|yaE z@{ps~44E#ulru51rKGyejxSSO$I3U?A8Q>DuRt9<5sx(J${;wd-5BEpv&n*)ky}qX zR9LB$e7NecT_vGwxdrIq`<4FB&j$}2om1z%`$I&Nl3T$r6 zad7kzpZwrP$qtmIn5d+03ZSCrT!ogTJHU>ECPuw4_Byny%LOYT3(9m#GoOCGBH1VQ zm(IM+>Sa(T#I2+I;bP?}8+T3Hhd&1n_gW{4Ay=q|K?YaTnrhI!{bqCks>c-)$~)PK z@$}^SIfBo5>!l^-W1edL55NQT!hYI!(@O(XLmTL!E^ARwGZ2LAJm8*{?Ivnw zRT=cbZ>G`t-D(0~bEt!#iTynf2;?pYwLWo3;c_#s(OU0{M)Q3+n*+UfpfMU>S^_b5~qp3gY(e+{j!N^4w8eE7y}111g4qJ6H(`MC{bp|o0i z_!u+K9ZMf=du!kN@}2{U-X%}Fjr3T%V!JB|)=hz?*g@ zf;-tjB&9uE8}Lq7k`zm+$lt^$$RIx~IV@A293?d%U;`*CXF-suyJpH{>I4PCl#;OT zS|moCJ5RQp9KdkuckExIhZv?D&wX2_k`dCZD9GLxzUC*bmK7 zQB|#>CL9d9OCrqg6XAwpn|wpf@O~x(z7&)AIF;^B=d~^ozzAc3e}*T`1BK!Bm6LM1 z!O{h$0#pck?T1u_uLN}Z?W_nKN`4#9Nlj_lchGhT)q=bb3|<`b1~*?+;k)&s z*^Esa!u!%Ey%N|q$K~LSM$gd;iijd3;zx9W#$S=y9p4C0@=59@*O}QG*wWY1J>Kza z7zxY3MM>C*IU{UJoPt+)(^_{0{t3AOH|7R&dAwNlE{1t9n*{?h`e7U4y&h?qihXy* z7->q|730~UsV`9>&!5G9u7B|Co|5ESrithD%?YJlZyCIf-@FQHnlYC(x)Te&n|j6+5Pkw(aU%mn%LP`VxhT$W}$A( z&J?3GxOD<~6p0dY!|wF0v(Ydg_ok5&l!>-<_qHy76&K&?n#CXZUTz#r6g+x5p!HFnY{ta;yL>cwQuWzzVD<;a95jZ zXK2B-E(%8tpHcmJ@pB-IO=haZWg3$Gt;(Gg<`^C+dZFOX7&s z0m33bbUNlk&{L6sy`C(;I$NUe*ZJ| zJoORogEk*7H@da1_BK*z%?PD86POoV-d`j%X77b<?Bap}&2EXs#p- zWkKz)S`dUWs%ZUggEGc@#t;PA8q}FnN!<^FX!DZNE!Kvs@Q}_~OY+{9T|MX)`5MmS zwahpp4_KY0sLwxnV@H{p3M?X(VINQDwy&T-o9n#|qbEIn7;W_AB!rJuK#Fpkp+(*- zhrHl@xfTF4lQPaM4mtg4NqTuZI^r7_{ZlLd9@+liU*?RSnWn#CW6sQiy~QIqhvgNp-Wtwr5_)tZ;(S1#HC!~El-ks4SSsqwk8(Gw$q$nfCk=Y}!fHuFSiU$+i zSgW&sO2|D#!+C&dXwBfW){0O0tw1T#Fyg!~7@?Ude@=W0&Rg7vQT@ESvhp;+M_8BP zQ4cf4K}Ra(Ed7}!E4NC5yCB0uh*_y7K+hT*>B+VVH-Ox8%#V@YuO2hHjHa&R+C3_^@ z2pLR~FBuh_XlZ-X!bV8CEe~=i&iaolr9Wss{TA|O8tURHtE;Gj-{GOvzHB{9pqYwg;o3Bok_;xC^th`z}%G8P5 zuj-Z1aVvUg?bFx4M%}IX&NWnPa(y0>t*(OrKD{?VthV%BwS(*vt>SAV%M6zD8}}u- zz3_^U0uJu>bxt#vVilOD9TrM?ByYre=1h;|I9>Te?Zg$B`GcOa z7ka+g+?-{=-M3BndgHkvJ2b~fXEHheb7FLcZEUoTBqMEGUST)|T=XuYXQtc;5W)U6 z`QY%~(7BzZh=ZMd1D*Gm=Ay2($#n9*qi`lkBdI{OZxz;cyoI4FZ|By4$fs?dSbWZ^ zzJ$wL`m+v&OPGRlJvP-y2Gf-aVtUWuN^OJ%2lIZ!Nlu5zZOd#siGE?w@iStV4s~Rh z=mfyny8p36Agz|UemXl@bg(1+Qvw?@*@@A5x^W+Y>z>!}vNx5$-*|+<69=7FZ9fY` zf4g4*jEkKHZnDbbF|9oD_i&o$VwU0@OF+ne zgVOrW>e@{y-2R>AX8U5U)J+y*S}(IV;`W&ptVFJqrbbB=^N}?C-xP@9`*r4HqNjZB ziKTA8zY5)AzV8u3A;+U2Rj8k0Z6t?(P&w~lRlh0L((N#*<+keB(M_;$dix$n@7*uZ zy42$29OkK(<@(2vYI3!43^RDfVsQQA8$$K4-ui(@fQH{rbHFN@+2T{svza=)N}$Ky z%sbMEf%@V^1EP1KH8tDl5i>Hz1$dp&3QQyb)mG79AKvG~zZg$x zoU{OWGmf;3i(TZePoi1MQ)iM%SE`y01G%Fs)E8g7uz@a&x^Higi6FCM7<@L743_Q1 z-Yje&k5Hzvu;3nVa^?x?E@$JX*CUewlA!9&H(YSA0y9f!^Hg+I|8d{ii=gg<+4z$W zMda77L45~RMm5WE)<*pM)$m42DcN2;g~8i)j#{p%u3?93D$$2qhgrUIOgL{`Zp;^JquODm@tiqn#o&DR&`YC0`W^k0b?Kq^G&+VDt~4?U z&hol}^pB$(td}Fwm#a@NSfEVjsQB#pA$9Xg9 zvK}wpdSp`a_RI9|KG9@m6aj~E{v-9b@GUnf8z>IFfQRm6-ct!vD)X;>7$`+UCuX58 z6~m>nw=~L&Y*^Q%b}EQ_+88Y5NZ@z-tGFj3jCG~LOyqXryR`}Myp{TwqbJ@`%u z6eI5F1UgQH=klwE6*&6r9Gh>HnNfz^c)tD6_XT}x_>JxN4eGb!oM-4H7XiINjDE%8 zrjxH(?#fKYYEFf|t9-2zqzyc`*d;Tvo3CKUJ5^2Tt;%dWZk5Y(fC}|WgMZ7yy{p~i z;}gZ!g_KdO=_(WoF^EM3m8tOb!)qpx$3WZqG85B@n!2DRe9Y&)Jk_SVi|42xoyy>9 z{b2CDp1KVJS-a?XiV+3b$#%=^NAMi*G|=Rm`>+5eRoxIJo+3JBN|UVgSeC-UbWbA2 zs}*m9Qp9rGhnt86L1hR5e`I){a?8;s?)D9E?Z(&eLRRr@_XQm9?>q%_H_z8yO5csl zkj|Pjw2Z8h4&W}L$ZMWvJ(mqWDy>`oT0vtNc?(>t3F^q8a3ic@GX8ioGiYvA=BXdx zP8Pi4g*zZ@`;1ob8LcpH(zbf`UbwRzh0omZU@6*Ox;J!%bgAt~!aj~$F~q3mYTQbT~&I5P9yiYjVD$vGUb`vzTo;T;~Waz(Ujd)c76r6;;82hDG=Tk#LT>35=9G zl18N%)sS$mxQ&Zue^uLF&QAq=k=!)L1pC-+gr(aRHaL9xRyYjs~gpJ0wg zpN)#8lb6-lUthNYC4H`QtH#}FL%B9WbcPUa)Xm%ZS9l_y3YU;2K6-)e4}0pks?sHQ z`R7eUN0gigD!r$~8i2m#bPL{BA{$O=OCNwn(~F0?#Ah;U~F_ z=E@?A5Y1lAryjLd?M(QCIVn|_C1`6+(E+Y@+U4rKUiUJU5x#^I)p?hvm*e+0ww{ zq4B|7N||TRXR}W7pINWZMArl`_BgD+nE5Dr!>=InjHqE)QX=k*e^uiDqvWQtbvwm!MJXseFP;Rrjf4snwID*uiihLlBB=WsJCC<@A*g8 zUrBn!%kUSHi_w99`4M>KL*9kkzoR9+QXz@vVsbw^X;_Mj7Z#&#la0QxNf7v!^r{(E zeX&u{p_Oo75VD1V)-xf~w6+pFzV*z0Q-`hv`|~b;({;!ZuS+^JOJL+x^Y8HCS8A~Q z4j*BzD#QtGa+W)M8ROJ3fnKLyd7IX7;*uiNZ5(PtU}0~FZxwqa(cH= zkn~VyWr8DEes9y+3zyfV_%0f54%aeaj|?KoAUberB8|KhyTEH2iSI{G6&#GQAxhMd zdv?*M(_z&UFQa-{_`Zir++Qwy5zW@(cA4OYMTRlQvZ1%_=f?T?gBiLIZI|2bXsn z6E{NNMEf9vt;(c6%P=v;;tU&~SPpRe1tx_a1b-cMm6<#_f>iAMj)ULN6V|8lHD*0- zA%fu&lNytdx0lUj2Vy7xtlPWrB2DhYAPmdRg042uq?1wRPk23?(<{qguP9nj4P0-~w+am$UEl?P%C(>PDt)>1`n zHA`8SDCAikaIvq8->-;Lg4P*J0SEs$cb;Z{4crhp{s?YCCJBNaK9c$FEOH$`e*x`_W%UtB#$+J z_4G-1=)oLmU^){%^Tz;z@xrn6&Wf)OPDmj!#5a6R;)9N(#;p@GnDal%4MiR=Rk~N2 z8}3U^>-yLC(VT%*)jv(I-i>c8;Vue-(?Ehf=@EXC&$@7IiJ7N+LovehbTcz(D@+Ih zo0#hT^)7$@xVzlqjXNUN-#-Sjw4253*~^MbOthT0OnP$q>=1|D{e8)C)oAzr z#y#6d+OMhvJmVR5-~I+2t0wkGiZw%|Mo|Qnp7@ZaS-F0c9PVL;w^#h~K*LW{vfEo@ zne6#az4^=Omo)x2B3up*PPcmnPWlp5)mlZ6r<{L%rDP@~O&b;w7Z5?XlF&{HgYwV9 z@1PzR#j@r#jE%h@d__c;=e1xaSVFgLj2^7T(V=;56bd$>{^7?lMYG^)4s5FPPIu$& zxD7?gDgGcuso;y<#^wjNy)XW3b!qwML9&U$u?Kf!{;sdHX+y#u)O9m_JW;R`c#pgZ z+XAW~W`NmMdhV$*pEaT!7Ke>`Pj4&8oiq8M*C?;QIJ~`3(!ACv2fDnhjZQRUKSfim zYb!g@G%2W_rHjyss3FlAA08O`<1q#8&Flyl@z!uvs3w3r_grAQdOnLs|W>` zxc}Jc!%}nOf#N6q}!h=C6>pjov~||OtCaA zOO}!VmDKvf@^ut-tDpG8pDViPI1o7vDg{_aVoJmOi-beaguekB>O4_g!SHj#Q#8Ni>TwcgJ;Bf77ms zv)w-SNoL%%6#a1aRy@o;$_Mq%; ziBH*aW?F_%3e~8@WMQ1A{7~)Wei9y|Up1wwS!!av(6a_lAvh<1=)8SLbqk|8KrjOa zvkUh>Up~e8nW4vBt(rsj8dMy3;dp!Br_%fecFVKK+HtjLnD?GQbPQ1&beDd+^5Y~s zqmY>$no8;QS3&>xtyK`4bLJ=-*Sq)JCs0e|2(V=m?v1mAU)-^2U{QYx;NS8pjm4|U zTBbSx7K2CN>(*me7L=+{!or%Lwjgtn)rwG)>s7Y<F9)>%6jM3K||!9*X;HTvHnxOcy2{feCXhHl_ELN|pp0@LD) z7wQCIS(GV8QAzKW0#-vX8$$xK_O%DgWzX1Aj?T_eL?@Tn4kIWhR0(-{wl%9~!X)H^ z{fbp!!3R}+WQ*)U^kRTh3pF_tj(07%W}3b~YGxG1)77Zt$Ejh2(!Yc)*6(CwT{c@h z$7RIICX{V{s&!kfs$*HY!0G>1$6!f-_06HfWT2D_AYlJ^3|o_=6lgkQsq+_4AM9sK z?uIoGBVFt_ehqCHn>13B)l`)HMWoOWs=Uv9`-azKhkZFicIWQKaHj(cK-bl( zn>C*ENqF1>x`e0q;ZU^BF$Xd1bZ05hRoWj|UXU?v-Hnc%oaJme>#vT<4H+;peG@ z%shtQOiVptUIlyDnq+)gEA9O33_bhyw$|)$`sHBa>Y)4s9?d%L(R}t(ERS^9wzcWF z)~mxwt=;x7$B-ewYi80r)DzU`A7c!9W*x&(*ngNcuQgRR@ge8yNR;+4*!tHeMFaOzbE6_YIxUqdLN%} zmFOLN)@)%Qp9&~1p+e_1uF&}37BQEJrQ8;t_afv#b0TcMl!`L1))!1);(4;UQ)UKv zgD(0}@o^``?`}ol<@g=U#{Sd3);a>ac!OGIgQwzQ;De4Qud@5v>kXre#noNyus`6^ zz0`?*1rxj%N104<3r*=&{9#UTeGIK#X`fyQ6^|A8L44Bc;<{k@Jif;VG zGZDEz*s5Bml(peS2Ifq#&pKYPJ9V&0Tq)rT1n5(n8sUempevrWBkFfCBK|UcPBImL zqWu`a1HRXBHP^Mwx2WHbko=mCqN^bA>@lD@Uo5g0k6!kR;Xa;Wj<~(OC);X^ygsna zK3ugK`WxfV>g%$srSL^tri~A=VegfJnjkpYt{05O&9W-Lrmi43v$%Vor25otV-aJv z@^S6*I{T!2JR80{l+echld+y16Mlw>#McG+kf=X8X;62YKE6}u)0$3iX^iPgrTaRw zWb?1m2=!+kpt#}43a%P}EJT5O9Go3acEK&N-0TcDb3EoedVpM@C4A=Lc47dO`wn%R zOaaq#Ns8Wu-BsJVj-}CY>Hq`eGcRNh&?lM?oWyvY{6iKmtZBna+_f2wAddVgGORP; zG?k#7OY#6a?|^#;&I8^9l&W=sURAgT18q&D5W6Ep{5-L6lyCr$w=x^;&x}Q_)*$y) z7pX#A3~XN?qh17Z z$6@QUtBA@ta-0N2UL|y3vi|2tyFdcJU9r5E8opc11X=_=#0P^Fo2afI<*A(0zEEBz zf^HJBdGf$w^iS1?JNtUeF1@7NYKr9>t+6ki20c(t`77L?j_JT;h|AJr*1NIn6_I)f zN_zOU?S~iqN`}x}KqeSJ8(Q2HqW)^Oh;YMgBBeF$IAYrucp+%bRqT@!4Z#@fu9Tqg zbO88(Y(LvDRJ9I=-9ZNhbb;&DHg7@{la+vKSfh@;h?$X!nMj z1AEzCSP*IM;vSe1P+x7_W2Hm7L^EzWr28t7wIi=Sa1vl}uZHJL@NBwPn*mdv`UGJg zL0T1U>Sc|I5zT`nw-G*Z(jD23{0N?aRXqAM4%c_ne zE}lFm7h&gdAQbItyLbu`3*hzLP(1fFP$4HtqfgTAb}?J4`oLuaY1$aj{Z_vY6aT#| zxGFF~n^^O)4;YJh1G?REF3)Do|LbW|X(0<*0x<)S23rC*2T=@NN*4F%iL`$lls?>H zYfTN39hANJ+!M#~=%sCY7V?-25QW@(*OQV$K+0|^SCMd{6Y|B$u#FxMK%DPh#D|7i zeNc#N4Se(3s{i_Lf<)V=1vK|X_sD={`No;^kZ@rjzK^lbfYG+7qS?d4P;VHB$ZSN8@a;&= zFBx4&B(cQBz`ZmIF>}8aO@)Mm+8en*;xGh%`rRgzOF&!|98y1xu1o4*)1Q@@E=Cl<#HO!m;@-tEYVE(mPi(~8FZ@&MDEhH6@p#*MdjIim#BYiZ7UlAu_%lVq0J09jo#92BvbgSw1aGZw%o zPVlzanI875P5l$Kr}n+ojd=Rh8j;{P;W`s2yt6FT_6)D~BYY3ws(Zg<38e$*0jB`+ zK&a#&5o6LnG}}X6qIYMPi0#Gd4b#)uIZXNr?CpND$drJpsLlRV`@5k($=eQunKSQu zc&Q|E^$$;6Q#&BCc9yaY!eDY-yJMCoh4%@RT%Yq-`a;TQ21e~Mm_(FX`H4oOGQF4i zJI;QAlOi?@AC3@Y} z5;hn4H08BxC70Yr_n3$HO|cvPYQK@M2|RJ;*qiUZ|GURglh|Dzg?P4JKE*^BftwYs zL)Jq9UZmpNOgw|;K;m1R;gdI)KVrtNLr_`_jl%jc%X?kP&3s z#W|s`qsEj1X(&GhlrukFi!b5#PO+<^eS%C)hc9@pJo)OipIu^aDPSPGn7w*6CTT2@ zdyhW_CPM-(A=_`@y|*W6W@PP*P}$kJrDHJES_<=$6UcEsanT?XzM?|#I=TUm#Hw}{2#K=3WbhKCo$1^twwB+x8`?)vx*d&%LezE9Jms_uy z$LCkT-Jr3AlF*R|S2MHN?2AQ9>kYHyW<#|Y*R!S{3ri1qFkC%gNjqSKvr2MY8KSTO z^AqJlo7>xS{Y_JNS9`haRHX`fQ6FQP?MHfonaA&(-Q`lwJU(;4KsL`A_)9O=X6>Pq z|545w1sdpy__$h;j5&1vo{pkF%KJ6PuYxAOeZ~4y@JV$?!3yZxmn7)&zXeoo{4u6) z!m}N&KGRjy`GFMfgah#;C4v7SFG8xE*Y?}V^V^8wC=jpZkep))-!&x z+@LtM&+vs}DZM@Eq!2L%Q-}0-!jN6|QnOfA6%zD*-^*#7-1j+%a%^7`nIEy}Hi@~y za>!{We1#fQAdQ1I5?A50bCO}#OkQUr!GR#17IYJM5Dt2{VP-yLe0jYYr(lMX{7R5O z<>abp*6CzYtSId9(Fs67eJ_fkO3ZTgw2)9)(aGmsZ5S$Hw)qxmg90ZKkN*0no8P-R zt{h>do5cbQ6MI6q%G(_xpu@fg!w9H$!>`b#QarR-8&aF(!(+V&p+479Cwp-KDD!j` zDZCLlwzNuDH^$51Q2QDo#{61I!>t(S;d0xFGJz!tlMJGfimwEX=Cb@o&- zEzEePsfE}#X$E|uNIs!0aYk;+yl8bV=`i6`+#fvrG=^v7gSW&jY{HMm;akdnDc2LvHdqgx_uD=n-(RWj@)L(+OIo!eo3tCmfs%Q+R!||su^GpMQ_J#w+N!nvb~|*+C)+p z?KYQ?$)9M0|NKB7tmz6~Hmd1|SZi@iRSj%da;#gwKeR`w z7>Lzd#MMV>(m&>p=GMTF{k#80w2n;p#M1c-29pXF**|4;a>8pJQx{7j?Yx&c`O81i z5>vxdqwYCVH^1cYSrupoQ#;IK%Z>J9=A}_J$aIJh+A*bKc;-y*Iu%7deawe>@9gK6 zg5zxFVo4&AF{B+yniVL=Q;j6dgOV{({CW7ie~VK@2q-o@@(ZA`7!lnX3&wv~?L&#^;s zFRf%`oMW}L+&d-3CGjt_8RSQwngbgra*<ipFIqBZzf;eEZs zZ+_CDG)8K>qA2?Jp89*P5RTID$W~{I*E%>(h$Ja0<>~b3Kbo2{93h%D=7g_jQrHVx z9T#Vmh-rVM5zxgNFoy0ePmR!>&M&ExOmSF#B!N0bxw#DKRIWHSTz0=2H|&_NnE!wl zFM;*FQBl9Ul=d^OMlhuh-krSWn|9o^`x(ZxgP5@PSsG&wXL*C<<8EvAUO^4p@&p~j zZZ>{3NJ;_O+Zv`I3i$q=+<&LCtq!@Za5SACnJ|^kI#*j-NFrS;8=lBm*i-^^+suEhusTp~YH4^x zay=z|Byer|ppr+dJ!i?4qiH*-P$cLgpYIcu>#@<_Tryn}4& z$j@-s+dA}=k@W(PDBWr+bcv&qKSZ;;qCqT2x}9w=;<-%rrL|OJ*hvNq&%sJ9pAp*K%o1NM zJiq?zJKH6SKVA_GH+20G(RI31vompzpN=?N9oguF_-|ZPB!dNKWd$L6; z(@xa$GUpH9nPWXfm%-$n{x zaHCL)jSjhX^2kPl-_GZ$) zpMViy4Q@{_^%M5xPY$9cgo_?pNY3v0L;GmFBz@+yVXh=k+e6h@WUvtyj*23B?tqec zEqnh6hwQ%_VRnNwn5)1D{|GJ4F4oh!Rn$H;&{rd&tsf!1Cg0X_-Ca_~jPX=UYUk7! zno3t@uUyLx_%aQATEW=X@SBSFvT;F(k1|5B)+>Iw!gz~X!3%gViW=Z}qM@@KT_S7G z=VS(8N;bHMfBlFm2#94QG3BEqS(-FNwGpb`a|bDVEdL7L z^sxz9xKO)6){YFQ(FxQllVlTDvnq>Ydg)mXz8Ar*zDg-!M!aYUeG9SE)4i`}n9jZg z>N=$22>I+320IcR3^eOye)zfGu+*>F9N;e;1>i>d<-^DEn!Xru_4(>ZCp*QdAnWEqrs}y^$d9uozP~v$b;B@+&;M zn+QP1AzU0@Ll%x|2IJA`f}Pg);_8{KINMVRr`Z_gQSHpyWenqqa-fNy3e4AAVRrVz zo(h3IBOf;0TBa2R|C%z@tNT03uBrh}(XbE6AYi>x(hpH@HHdLy)F$9hl}JSWisKp) z7{Y+eS>Ou=pJOoMHh3TDO^zuG-nqJ$*8CpUN}k@;#(t2cX!35FydE}XjPL-NZPP4$ zCRGD{3ETia9qW^2yL@pq^@Jn{ZSiFb;Urz97x9J;tvZ%w{iyfVCuL2vlNonL^1m|z z{XWOQk~WW80W#S`&3tj{r)g)iWqXv%i+4@QlFDuM*-qh85fO`ED5@hi=)=VtF~Y3J zpg*28Km6Cwe5m?%t6D1`v4LNp{C@RQNSDSTQ1pya6}maU)g^jf0IIkvIG{R4glF5>Jen;*5W>I>Sm7J<@-G9&lP~4CnE_gP)zj zYve)~GvPp;e&B{abn=@La&1A{N?&}>HL|d;GA8ZOvv`xEHp+_q6kZdt2xWsx&$qem z&4zx>1|fKvA=j5o!Wu7hv2b)>w~*bp9K@MMTvA~V&xfyPbVmG&JSf_bd+6I}z+#C} zOP3Qqpb6!(&f}!r6*eg=9@Q)=`al}6Cd?_UZ`#isS_ z?^-(Wjne;X%h5YC;x()f>-$qt?$HI)H=EW97UROE@sI6~G+WgYcp}FnTcwk}Swx&Z zt1KY^Xth!1LB;5XrmgQu;T%Ch)73F|mwHv@XwHlFInUTF^g=Aq3@1zE4ITkYEr%g- zYrjFr7Wps+4Se(>(T=8+G%4TAR^D57e&vbkn{vzJJG0E1ma+pG1q!Tkza*ud6OW`D zgInWV*fZfd)O&MRxy~;0;~VyxlG`;wNi)gLi8LDDg-n0{T-zv1k_jb4X95->#f6al zTR!D~AB_G^md}0lvTXX191xG8FDP*I9Ek6C=7Qxa#$|C94s=dP5~rg(ZC#Y^#ezU% zfev@+W=KUzD?xk?E(?Vj(o`pQdOTczv6gd4X0@-wi2Ty+b6)L#NU;mGM*3l*xr=;4 z=;=q^+TeS|!8=lZkC?qdQH9HVmcI--aj6_8C&CHD#Tn2@crP2Bz0eCiQ=$by)jjk= z>l#_=v45NlWLCdg{|_iTfL$}8u8JtKww1>Q9ESnN+YnwG@O7asqJ3XbS1j^QP%ujwimNq@z&^`5;&N5R0a5TAD&egH) z6WTF#G8#`KpcUIe9z2@xn%*VK&)jnJNq)yu1O(MLncvCrd-2--F_8*1uXEu_8}cnHBF7bZnO?}1o5v4XH+V?C1>59rc3CW=uO3@ zfzuI;9j!L-GE*XH7{~YJI*yZfGun^yyrC_I38{|m42J=@RLC3bm%48#dy?SZ! z=0AwFm6Mrg5yiLeJoq>T#P6_~GKE0%UOFndKSc4sqvv1}^Kd*bDf5 z$5(hkmdc~Rp4LS6JR!UA;UQq}&5V>UI6x7|fSq?de^kP2^P!0BRd~#MwD|n@bkLNK zVHQhlaGt+ASN2o|P)^_^Har-sh(mr3$r?RL z&#J}qqM5nf&C~d0Bno^!wQl?tjhIF9Ae0(t);yGi9v>f)?P1C90up_zWk1$-yrs!v zmD_6k4kC<$xdeml!rAYgbybihw?ZihGwZF&l?Tp;zZM+Ib)D%b5D}T*dEarS0i`;m z9>MQBOFHSQ5oY0V$z$Kq>s_-H=UTu0MbAGht;nOnMxZ;WNN$FO* zy@vJIYn)=j>oFF1f*5+n3a6EjzUI3wGoU>bLcI!_)m$+=J~H$*Y|Hdr^%GBj-PpKj zV-Hlhb%|?_j$nS1XezyBSWsS~CKJS#vZxhL-e+>&zlUFr{a~&6r^N z%Qf3WMDsX@sL|6ND}q$rAItH!9|66K-LQAM=f%hJW;UGMx!5~c$fXD#`lH?wr8`+! z0cgH!`x|eT8*Qi%dACS0g--JJwmb0)sdq$obw$=)&+%eI41f|`6m2G_8Xu1OBmQXq zv2e7NInT-!+!x3M|K|YqR=V~!)Wpu=eiPJaMu&U_3^NA)d&7Mhie?t2ahvEM1AY4= zVEC9ISgcXYPYWh(yh9{E=4eU9aZ!D80K|p+z$l*1YO`3>$=)d)ImsBZAiuttw{8wS zYnB5WX96o99{REgL6t(}gm$qi6JC~tKM3}s;`=ku^^bq^@JhmBDXFjCnk52T3E*|O z_%~y0;H(X8wW{QZuD|+s(Lw{rl}LPMt$zLA8!3RDol10^w2C?Ue?AWuDloUj{qb)Yu-LuI4d}CJl&#BY8|TPD5~plj z`-LL*`|;FKn4K1Liw_^7VcF29Hz*?fpv%t#eg#{64}v91jnZJqj0hu!#b%OW>BliQ zu^sY&%lm#aL%_|L+4n8<@?H7qQUVX;ykBGqs=T)CasTqRd zF7jRq8mfhhXFVf8JD3OWJ0LDJ%d&!B)n3=b@q)s8mU=km6HZ=9*aT*&eC0fGkpv!g z$H+;XE>c$>*C6-6of5FkcBE-kB=@J3liXk47Rphx*Wd9gi<_U(3vZvxGw`XG8%pK9 zW|tCj8EsXP%wPQ6)#Sc5&S(Z3EVL=XOl{?nw?ADkf_|#L&B3Q= zw{U51beqB8!1ssEg!EAXkiOmRXaML)j0)R^EbZ!9NJW}d&4B&1!Q4kE^yi09BT=le zk)PzqK8d{wt=UKbe%y>`=c)o5?+q^Ags>YQxIdy4cyjQMm2Od6FDr z43-yjfE#=b!c)mk{)C5vf`|=G>=4@jn=Ce?=%K;~BPfR?e_V%8Sd)oRl9CkjANo=k z6F7wC5Kq$iez$szV8?m-RsTc+TDvL0HzxH)4zz4sHj|gbO_Rs%8n9b768-l59y1<$ z@z4+47Xk`F@&wF(9U@w2IM5&=cA10{{%3OERdF8d*vvgt4@tyuh3+;+48)OI=h>sg z0y_*hO=70UYLk!1|4jbCb4fN988?4ddx^XRfwFyr#?C^T_xZ&G0 z)aYMx*8lW7e)_BsU7rFTK&M{)^TFK0{8=UZ%vEjS^e1*=BGQ(8f%|3ainVOkW18$_OBpMDJsF}gIzYWMX?0_3h16*nd z{YS58_h|$y&A-E`V|LfW|BBgP)fwj&8Z1vv9p8OR;&A+5HiW({4(bT>a~w`!WJz3Hi6J z_5~Bb!a&2kY{3jIeE=7?qhiWt&PdFj^||M@m)4&Biy>`(tEMz4Bg+82%#AL)JIAXn?H2#U69=Rn$HbIe5~5>lSmc>~ zx=J@spxp5ZY(joX@S|snis!h^lYyr#UK=iEdyO3NF1*r=3ZqyDAN;Wl`<+~ji+>c^ zWM8~)ccrm%FIaeXek-nOl!YEp(YEVtm%1a7az`k^nsF^x!VkLQ%+Z)E+B#VZl}>g- z&p1J+%y;>huMPDo0U3UYjJ!+5-ov0u@nAR-WgT52x)a}Y-y(7C5f!5g*xI3~=j=;U z0=+J2Yzr$PAu)Ct35_e`XMC;O+z3I5IXkS@)bShv*MGz}ho!!OO+9ZZ*Ti8ra?NM~ z9d5e76y*~5rQ3`D3`m&Rjx=5d;kgDh0{gj!RM=r&lKZEV8`O;YjoNw`oP0O-M+ZFIPXFy6(0^RQWd`3X{V+B^YN)g0AVC$A zIxr|{%tO_8H2yr$Z*5HLjxY>}Qmue5P;zoc7z3?t%}UJ29aI&eg(v7Hkc0)e@-Kgv zxPB2Z>koJfJOU#G`6D5fnNN9k5fIS5sK|XX2jYk~IdcuanB-*7lm6U__P1Y_hlY+? zN4iJx%>i2oe^4|fMzrvryRX5p`-9>4{k){j(z{lJq~!ekBFJ!#yCdtgHl7h%oTeu+l)f-F(;1#K%H+&MTky+XGbLMJP^jx*TL$c(CmH9}0qG3Z8 zg`3uiH$i~Sx0Q%|{iuI#FIO0x^FolThh&dH>_M9=@glXc`wgxR5Dl?Qd{M7z*4IOS zN)OZI0DKnI7EGisc#R&)86zAsa`1vWJVxjKd60ONGIyyBZ+Qi-7f4s_W??XugHz?0zr9Mf-Qdgb<+FB z5ypZstJlRx4^M;u0Wjy5glkCCVT?$IbQIt5i@eDT?vI0F$9dtRrjCzPfj&z&XX&Dj zc*Nz8qKYv1NdaIk#_L*0Jbs_f$26j@z+IJULa8Pa=jH>(4=J;Z1HOw=RBDX9P9QGL z4HZ*uTGw@67OSA>S)G#4DhY@kzqavWelm~=_V^QIUF*~QXfhZGuh?U(UApl% zx1C!We85_R0&Ij+ZFWQ9(#(tTBT2ct!-1j@O2nR% z8Q#NA?tfG_WE)n@x$*Yyu@UZ+)uv2Z8R56iiZtABu$d)kw#l}Edw?Ldx4g5& zHly&x1%WE_nIN`Pf?%l(Z^`Q}`Fp-$x48#C4&}4tNE2|!osC8Fzh?zj-eO=0xam1H z&Q@GJn%{;TOG-{$&GR-OdF~noyNdY=+6;QM3ui&n?l$)+{RY)fK-#V++(xmpNWL`2 zP)k{V(ca9M8v8!j(}D7B-DYtcaeXxv!iMgWpk@?jWDBmc0Rlc51_bOZ9kU}eW>?RJ3c(=9Z+60Q0t;M^z@?0nR@^Ha;jDL{f zf8MqV`y9iPcNlJ$JIXT~OX;>*)` z`VG`4t$~K%_P(U?bmo(NBB|(m_^pLk>!$^Xp8?>Zujk;40RYA-nmo{|!B?d?-=X5s zYPw2!a*l6mngy}-O&f{>pj$3WNj!CfMOx(?y_*BRLE=zP>JGhVj$`1OXZMas2MrO^ zmfynta!eJ>UD(16leVLfUrL%tD1`H$+g{<7>bAj=?C4UehXh8a%=Oo+RvLkJDEZzC zxH;0-khp988SvwEsO<(0`tk3r{^?rxau}^XxgeWttuNY7o z;CFv)?jug}HTnHXbTA}Z*f;t*Mokc9T3}}c&xRMyX$+mdSlSSdU3=A;HAMIQ@gS3W z-zCIB65@^qnzFO+_DnB1*f0_w$?wP}j=+uT^n$A=x2X)~RZ}Tc*IEM&n_P*zHa00a|ux(I;JJ=jH^UYd-iN$PZk`hC;=U}=MRjT1# zKSpY3VLt18)Lv z;Wd*$jNx`=wKy`}`%tF|WaLG@5h(WR+S{InZ}8Zm-;?@KMTF5{g^ukgUN5){paf&0 zfHEF5Kf2D0o$Y#V*a=TgdLkd!##H-;s>b-tHoe9()m}kXt%z0 zTlE~~C`y&m!Q0U{=en7+!=54lkx8v3YR$=XxJ~!-mw5NY6dl(m;@d4wb3Lz*c>R!d zv0SmRqGxr|*Log0%rzu+4W+sMj7Kv`53dTSWUVQa?0lJtr`!?JUr*G;84gfQ+-J2W zOVFVut3ECwUw)_mHp?z~sG-f5-I-8NU)|7vt=>gB8k zjcCq+$cwYbE}LeOL}T84e|y*BkRqG9*HT5DF;=Tu{z2~7eP7yv=`EiBa@4 zEpOY2r;HgF!L&`a$`uIc-(=rCtsy1SdM)&DR2+`=@;W}xphFnJ^!^y#u5EeOU=gm7{W zilin&fTkgU_ML&s)qt~~#c68n@IlB+1A4|0K~aJ|fUu@gIKF%mO9TOvizA#4)9N3L zIl*?_p?!laiplGdUlkquX#P+$=r=}D%U3@Y{yU|)9+G}vza|N{L_Bm|`u}}w6p7G{ z%}_Yiw=U4qjq3(p_QO=zSW2(4rK!D%;W_xIX+5cwx?nCv-h08Zo;3g|MyI+Y6{t=4 zM1nbo3-F5JW+v1yBR5p~WU6uT6{zxZTwV`Z6Wgj#x~o(IYN0e{eX+ny%jQ;N8R~X+ zi54E;dhBf;|I!hc-*zA))!}C>#$mWJ?=P#WDIke?bg_e1Txt;@I$}U3Wa$?xp6?Xe z+{ysc6Vdq5To{iXpJtt1-a1m-DD`Om>p6+f0}xF8Kcocgdbo7z4YM1i(KU^(Nog~O zEY+O#9TEKaH}>y`OVUMXciEY{mn1qZy037iSb`R9T}N>VwTn8bo2hibRu(59s$Eh^O;hA zi!a8s?Q>qNfBe<_*OtQYO!uYhuu84IwL#rxPf9xJKr=Z#>AOCX&yg;kHJUrIvIZm# z=ykyhrXrGLMZBF$?lo+E(1W$pc)@w_(QMdCMuVd@8b!Q$s~zWTXnEE3pH=gc*A@ju z-5qHa)foTmBq`ln-V>{)MM&7HEMSKF$CH`7;Xs((HX*6*eCJSR!i{>#hbRfVpq1%G zKKCf7$vvMl4_5=kJHKfE8(!3dD`dCbjLSG$?B&}EF#5h3{vBFNIZ5RvOpdQd=(#<$ zm7#uC^4<449ZvNdmcHtMayjRn66>t+)iFMY7%r=muN2kq0csRhD081y7d^`6$!FUu z_{iQ(p(~^~LOu|sjw($GPFWVjiDo-RpeDSs2wTw%d{}0nC}QJ6WY4^`yD<0cqi{c$ zS;`Uc3yf)4{Y2DaLE13wG)Io-oaA{J^$XZ*6c5{^0z&Zyz zr*{UiAAhd7A!jwx-@3d|=Uv&Utwv8-I@W2}^-%Cvfe@q4E(A;K#Xf*3g7Iq<`>t;u znS+VBr3`*Hm>XuPVywtG(7zTo<$Y5`o$@!@FWNgybJ0!)HX4?D-$CeQxZC?``0duw0_EG%{` z2vRDx`;R3Z(SUVt)O;mf`*uuiF{x>)qE_tq`VFNbPH;)Fx78u7+Q|x$N#cKuL| zp-tk+Cptzp+xtPs29kWc-xiCnyqcF^~O|ME>9D7g`owXVfr0p*Q<^H0+-L=2qC?IV$GdSQ8Nj zpQkJ9D>}Q=ZdsnF@qzL}TOCThE8eSNu0Z(5EtBd%1De}un^bATy_-=U4Znw7#b00A zP$NZ>x&~e!dvwg%v{!IM#T;h{e#QVK=1#mGe0`V=nR!{OsPd<@niM}vW#*$_=gU(7 zwQSjMHm^z}Y<9lJovcz@FPiJDX-+!0JKoy}^Q(?SH}hbhH|Iz=qu%>oz><(0YG29~}5SNsCRUhd<$xAg+Nt$S!1`{&0S+T;Z~cX!2!LH>)@BO3(sOPW}<9-h#d zm{!%AQRQCtL}TRIx!``*cf+f2>qz#ng@)GV3|j};@|z{iFx=o_FhNbjn2zghNLFae z^Un421qwwrUW+%B<{uo>Ce+DeuFW%fA)|Rm#w(ko-|_24MeCuMJRW1M@Jil&n}1!? z%zod?fxjZ2;kWf?%XHP1=07K4pOLAb^<>(FS%%Y_MQVf?TKU1jra#5LY-tjJsMz?M zZ_eCaUbIOv%N8g5$0}=B!FxEdpEW~N?qp&i`{2u$oEwTRfYxst$(Pj2TiyLe5%1ZU zFTX%|F871y2|LtNUzYl^6qfss2pY|-np;}oI#~sq8~Ze)6?b3{=k8W*66xfuY#Q$r zNtkQm!f74L-??F8P~HA|rl&*FGia`&jIe*jRaRR9@10FS;E7XQB`N)^RmNJIa;4-9 zfsQEl^p~wKio#G?Bkk$mN&KCT>v`aXbZ6RK_ z{hYL-p!0S`TszV*jO<&&z-Wr6JzWo~x}sGVg_0?X!QV2@mO7HcER*^4oc5-YNEfU6 z|Jmwpx66C~`~8Et&9ZMLl8Jp>^PJqrH3@6z#gb|<28Dm|Y@Wg^RA-Pec?Rk_TN3w1 z;cc|M`(_^%t$<_QQ&A}kJg+(mxa<1ou?dy>R~%E#w7o^R20&nHP;wr!N1<$YJzVpcYxMi^VJpw>ukGs^?-z z7%7=~$%u3Mro*$1jq)Kc+3_kF46V30D(5gt`{ee=30oo0!S$EeOatKbG z#ncEc+EVJ#6gDVV2Y>o`Lo0sJjHW`)82Z`(??+$U)1FlN#}*utDLpxd+U2*-GN~tm zij2hZNpz*cLoIN;d%WV|Z(qHa&w{p!0d4FVKD#|zRifu9^^42no=R#R^!rcHKs5{Z zRWD>dN3m&uO$!oLz}$pS(6(2CMst?i8Rt^8{kPG~a&#rz9BZs&dMt(i5mO}w)4st! zWl4v3ZP*`kd&@6>EeHSPXts>pRX$|}2a^{ipv9|T|N0oO`~B7^&%-{sex4j0VZc#t zin)b_K~0f$T?UvLHTLW=3w?$!oaHkf%B`njoDOreqx5PHL4&(l7yc2U2OWmM49&rz z36N{c`Sv~kC`VRL)c*^`5IXO2&!i)1NnVTMXh}R(D^XU+ssgNlZJW52&_-w{VUkX{ zWr3{+CM1HxgeI9b4ya5Uc*BTF!h``#qLw0^E}~;@c;@P))>8F_OqWzT{gsU1x=_SDPfm&{H$6cXMs={OsPybOLFvYZe}qxMVA>&5!nVXFv+erGlcNq{Y$1Y@!#9%uKAUxHLrkb3o24!|~AAAZ@GT(NBJA zc;O$%KdgKGhc5{qKKDHN;VaLeX5|*FeJXRCPYYu#V?T7C-`;n`ucOu4}1Oc z!@@nAH;0p-|IBdfe{8ZMNfEjKH7*p@tv7jETL=ixu~o(GKQ9*l0ZJuYVX8Ig!X>Y@ zBMfs`_?|>o308HvTk7&Gt4m38J!vhKBU_=*JX_24Oks#`hCf7irPH|rEHe*IP-GX8 z*V))Q3$`AZVnTA2w_Snvf3VqZvv-$Z!6>O{piLk4JtkIpx^Qo9%uCd2NP{yWGk!Q~ zkm?=7N@3$wK-;0Z*lL5o4v4Ftp7|Rug@r`)hyxxQUjF8Hs;@78;+k;LnWs6w^;#tc z$sois>i$!*oQmQLG~wBvOd^7)jNW~L>x^Q_{17CqY(ffF=wDdyJZ(H;f2qqEO?8>3 zxlwts*7fL0w7SGbnQ>VWkHjT!y!$0Zl8}O~?tgJepLMjL5CZE0=IKWt9yX=A6vbD>1ymqD%fykawRViHXTN9C z0s>uf7ilZ8;7UNzZQ~$63JHp3*rhs*>wQc*Ah@iiECLc`Z3& zRb4n5fd0Y8v~1+&u`#!L5+jC{J6z2dT>;${vX>RgrKuHsT-p{@Pi#y5u+uKPhPPfG zf64o6pa0wNo|EFg2J_{KxE4v!wXoYBdxj?+{QHw4EtG%$%HM~F{pzDzq$b(_^yM4F zKEJmAB%_5gRPV;?KOS!V-lp)D%dXRV*7Ulx1hHa|Qvut@x~;_`w>6(BX+Dm(ZQcs& znGQutz$Lpo+HI5a8F%BBtyI{FDrwqhC|zwE53F2Al=GJ z!ZiAc6hMH735L<3PML}{R!p$I8tY#HJunq}fU$AKtrrUKKtPgG)Y=;14)?x~Cc62q zyF2}=_r8yLT=;|2&gxx?&t7x2{NdVzHoV>y=e#T2aOH`I6cVVF2WvGS- zdRR?)vhQN4CgT)Y+#0`S!Zn1M_$7A6*P>WrGZq-NV1~q0GQFZLz5;1;V5e7;o`G0D z#RgmTMWK3o@3U`sk^PIh0=tp)h3oz*JobP7UL(JW@%d{%8XosM2R5-A=|lBCa^5-N z|NG3R8ttwai>rWco2imT6Vwbf^BC)W`sXrZ)*T+3XB z^J(ei_9zRwy|IyegQxTc2>yDEl)oZ!>5_OYP-X?9Rm2LA&L1XENt044{hP>4pcZ=7 zKx(u7z(m(ck>Qfj5!!Lle|Ng0W+vwSTgeo)M&eQ(@4B!Hg;6V*=05Rc{-*ur~3FOGunm8nFm|5$M8sqQ~`IhrAD1 zm-UQk#1%-U^j5}#>&{4%Br^=59fJzwqTgP}#D*)hNs_Zo@55At84&GRHg3A>^)={M z1?yMo6HP8ER{5^s);funXv#E3;tRqrNn6vV$PyxohAY6u21Q6pW+^Zdgx#vt^rF;( z9vC^)1=ne`>p97|5fcxH49;y@+v%i1ZX!}8Oi&@(;UhVXQ!e_Y@3IlK)Ocao>Mmws zvJ6^XH0n=b=tIb9%Z*7-qt1Rh=cTgvc&6B%g{3a!Qb>@b&gPk(Ks_9}dDTd;rcNUy zT61EGy_tMgFot%_%)W5w*{99$Bzr=4+ydE1zDnqK=29Oa*b|s5wYYPHW;LPrOg!0u3dl<`is+H?@fh`3jV>tSq#?!vs+^ zr*`}Y7Cs!0LnE8}CIQQaLrEF{X@yBUn1wOG_&^nNO#h{up@f;rLfnO3b4)wjFv=tm z5Ch5_1|ODP~)azt$w-2 zJm-wqXOsC85lUs8UelB+v}%V$nd|z4p;a-6xs1#?B|K|N^lD-wTr~gcvJpa(7_6Es zYPu;Hs}Lnc!2*s^U)xAaOq=GQ$qsGi@{5?~eZ;&N6Zxbgai2$ZS4a{ggFkGRC>(~C23@luoj3ows|50 z$p_-~bjF{Qas{1tI53GJ{hx*ii}qTG79bi;f~6&+w&-p}Y)x1$>7ga{NH>3@!_+FsCBBX1Ws0~xrmjlA^a?wO|PdaHP>ZU0in2*PP z@h|nqdy@4WAh8}}S{zJJ;u2Kv(?A7JErhux%)9|Laljf=ITa85u60O#YAUK5FD^<) z-2kYhyG4tJ2rc&w5Z0g&(xOA#=fmnFKf`NmEkbmedLfgI+=(mAcsL={LU5i`l5^j` zx-@B>AOMuUh@tIrr5UWcfNfcJFQE@Hp|yQ zY!yhIJgkJ~Splx!m_EALv~EbEO^rtZ%8MKiSQ0Y1lhHTXo0ME%ish9a9$7TJ(IxVN z^UD1*T#1J$A|jr`TmXWHG5q@X#TVdq30%agOb^sn%q?85{>~ZcaKqc2zCihV-T+(!6UdWl>f^uPU&rz>-v;(}5|yA*A=E{N}{lT3g1s zoXklAlwlxFR@YSpRu$L=D&Tg#zaPkLhF_1W9i%4SNuts=P^wjms|u_tu&x#8c3{FQ zID2O)KQ48Po;Njv=}suxFbJ{@FG<=8t}3vqz_wZe-VwId{8xFeDzNobAbZ>|U1A*% zO!lUC?bY3o8(DTTLb)0$=p+|j)KW+#+QI`{Ib7Nbtomnpmaeo`CED_2w2E}Ke(T!< zzW%^5*7F%3Bj|o*{H+HjH7N%r{k!6TdLy4 zm_Un4jZGawFk@~p65=%_H-UeBlWj`a-nLU!lsv+mkNGs7m+vZ9(($?;@8jRw?TY8V zy6UaB9OA}k>OarVb0jZXsQQeyO7i4UXKa}J^!7c^<6dUjai3R?BRA9^^n57YDU{a9 zu~j{{L)+9X$&;7+pLI@OgtOXP7Ja`4rTQZe(uo7IPK2P%GH8deWNRAF%XgJ4=}gm| z#`Cti;;`q3|2|?~bIP*+8B0!wJ@UNmHk>+yC3(*y4v2xcIvkipoZny4w|=OuHac8< zSs-rZsC|=MXcRMcUJapil%KMzs|6&H?UzF~-qA!&fV-nF?1`?YDR zKXf?y_CNHfLmPmI6T` Date: Thu, 25 Jun 2026 15:24:39 +0300 Subject: [PATCH 55/63] Bump golang.org/x crypto, sys, term, sync, and text dependencies. --- go.mod | 12 ++++++------ go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 08110bdee..ed9efcf19 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.4 require ( github.com/briandowns/spinner v1.23.1 - github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.6 + github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.7 github.com/eclipse-iofog/iofog-operator/v3 v3.8.0-rc.1 github.com/gorilla/websocket v1.5.3 github.com/mitchellh/go-homedir v1.1.0 @@ -18,9 +18,9 @@ require ( github.com/twmb/algoimpl v0.0.0-20170717182524-076353e90b94 github.com/vmihailenco/msgpack/v5 v5.4.1 go.podman.io/image/v5 v5.40.0 - golang.org/x/crypto v0.51.0 - golang.org/x/sys v0.45.0 - golang.org/x/term v0.43.0 + golang.org/x/crypto v0.53.0 + golang.org/x/sys v0.46.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 @@ -124,8 +124,8 @@ require ( 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.20.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sync v0.21.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 diff --git a/go.sum b/go.sum index d85757cd4..e32596b94 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf 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.8.0-rc.6 h1:3S2t7XGYxUtYCUnGDueJM1oJghXgIn5xOs58db4xQ8Y= -github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.6/go.mod h1:MU+YPxFRGFzBMboi1khtEGAoRwaF4yJX3KuBwFcfCKg= +github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.7 h1:3Zctfg/w0hi9/psUoK7aL0ciUAbsUvCmcEn1pREP0kY= +github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.7/go.mod h1:MU+YPxFRGFzBMboi1khtEGAoRwaF4yJX3KuBwFcfCKg= 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= @@ -269,8 +269,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -302,8 +302,8 @@ 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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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= @@ -319,8 +319,8 @@ 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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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= @@ -330,8 +330,8 @@ 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.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -341,8 +341,8 @@ 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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +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= @@ -353,8 +353,8 @@ 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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +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= From 734e738efcfbb40ab6a662add81f92fd783552f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 27 Jun 2026 17:18:24 +0300 Subject: [PATCH 56/63] Bump controller to 3.8.0-rc.5 and edgelet to v1.0.0-rc.6. --- README.md | 4 ++-- assets/edgelet/scripts/install.sh | 6 +++--- internal/deploy/airgap/binary_test.go | 4 ++-- .../controlplane/k8s/testdata/cp-cr-datasance.yaml | 2 +- .../controlplane/k8s/testdata/cp-cr-iofog.yaml | 2 +- internal/deploy/controlplane/k8s/translate_test.go | 12 ++++++------ internal/deploy/controlplane/local/translate_test.go | 2 +- internal/resource/edgelet_golden_test.go | 10 +++++----- .../resource/testdata/edgelet/local-agent-spec.yaml | 4 ++-- .../edgelet/remote-agent-package-registry.yaml | 6 +++--- .../testdata/k8s/controlplane-datasance.yaml | 2 +- .../resource/testdata/k8s/controlplane-iofog.yaml | 2 +- internal/resource/testdata/k8s/controlplane.yaml | 2 +- internal/resource/testdata/local/controlplane.yaml | 2 +- internal/resource/testdata/remote/controlplane.yaml | 6 +++--- internal/resource/testdata/remote/remote.yaml | 6 +++--- internal/util/client/client_local_cp_sync_test.go | 2 +- internal/util/client/client_sync_test.go | 4 ++-- pkg/iofog/install/edgelet_procedures_test.go | 6 +++--- pkg/iofog/install/edgelet_remote_test.go | 2 +- pkg/iofog/install/procedures_test.go | 4 ++-- versions.mk | 6 +++--- 22 files changed, 48 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index b4b010784..9a7f1298c 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,9 @@ brew install potctl 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 -curl -fsSL https://github.com/eclipse-iofog/edgelet/releases/download/v1.0.0-rc.5/install.sh -o install.sh +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.5 +sudo ./install.sh --version=v1.0.0-rc.6 ``` Eclipse canonical: [eclipse-iofog/edgelet](https://github.com/eclipse-iofog/edgelet/releases) · Datasance mirror: [Datasance/edgelet](https://github.com/Datasance/edgelet/releases) diff --git a/assets/edgelet/scripts/install.sh b/assets/edgelet/scripts/install.sh index 2b16f661b..a44dc6467 100644 --- a/assets/edgelet/scripts/install.sh +++ b/assets/edgelet/scripts/install.sh @@ -2,9 +2,9 @@ # install.sh — Edgelet installer (potctl chunked fork; upstream parity for upgrade/rollback) # # Usage: -# sudo ./install.sh --version=v1.0.0-rc.5 -# sudo ./install.sh --airgap --bin-path=/path/to/edgelet-linux-amd64 --version=v1.0.0-rc.5 -# sudo ./install.sh --upgrade --version=v1.0.0-rc.5 +# 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). diff --git a/internal/deploy/airgap/binary_test.go b/internal/deploy/airgap/binary_test.go index b6a050609..e55f0f5c7 100644 --- a/internal/deploy/airgap/binary_test.go +++ b/internal/deploy/airgap/binary_test.go @@ -15,7 +15,7 @@ import ( 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.5/edgelet-linux-amd64" { + if r.URL.Path != "/v1.0.0-rc.6/edgelet-linux-amd64" { http.NotFound(w, r) return } @@ -24,7 +24,7 @@ func TestEnsureEdgeletBinaryUsesCache(t *testing.T) { defer server.Close() util.SetEdgeletReleaseBaseForTest(server.URL) - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.5") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") t.Cleanup(func() { util.ResetEdgeletReleaseBaseForTest() util.ResetEdgeletBinaryVersionForTest() diff --git a/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml b/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml index d342d21e7..eb640ba13 100644 --- a/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml +++ b/internal/deploy/controlplane/k8s/testdata/cp-cr-datasance.yaml @@ -25,7 +25,7 @@ spec: cleanupInterval: 86400 captureIpAddress: true images: - controller: ghcr.io/datasance/controller:3.8.0-rc.4 + 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: diff --git a/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml b/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml index 50f01819e..44fd2fb00 100644 --- a/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml +++ b/internal/deploy/controlplane/k8s/testdata/cp-cr-iofog.yaml @@ -25,7 +25,7 @@ spec: cleanupInterval: 86400 captureIpAddress: true images: - controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.4 + 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: diff --git a/internal/deploy/controlplane/k8s/translate_test.go b/internal/deploy/controlplane/k8s/translate_test.go index acc28e319..f94cdf285 100644 --- a/internal/deploy/controlplane/k8s/translate_test.go +++ b/internal/deploy/controlplane/k8s/translate_test.go @@ -64,7 +64,7 @@ func TestTranslateToControlPlaneCR_DatasanceGolden(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "datasance.com/v3", crName: "pot", - controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.4", + 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", }) @@ -77,7 +77,7 @@ func TestTranslateToControlPlaneCR_IofogGolden(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "iofog.org/v3", crName: "iofog", - controllerImage: "ghcr.io/eclipse-iofog/controller:3.8.0-rc.4", + 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", }) @@ -91,12 +91,12 @@ func TestTranslateToControlPlaneCR_StripsOperatorImage(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "datasance.com/v3", crName: "pot", - controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.4", + 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.4", got.Spec.Images.Controller) + require.Equal(t, "ghcr.io/datasance/controller:3.8.0-rc.5", got.Spec.Images.Controller) } func TestTranslateToControlPlaneCR_DefaultImagesWhenOmitted(t *testing.T) { @@ -107,11 +107,11 @@ func TestTranslateToControlPlaneCR_DefaultImagesWhenOmitted(t *testing.T) { got := translateToControlPlaneCR(&cp, testNamespace, translateOptions{ apiVersion: "datasance.com/v3", crName: "pot", - controllerImage: "ghcr.io/datasance/controller:3.8.0-rc.4", + 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.4", got.Spec.Images.Controller) + 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) } diff --git a/internal/deploy/controlplane/local/translate_test.go b/internal/deploy/controlplane/local/translate_test.go index 7318dba6f..e666e0b0a 100644 --- a/internal/deploy/controlplane/local/translate_test.go +++ b/internal/deploy/controlplane/local/translate_test.go @@ -14,7 +14,7 @@ import ( const testNamespace = "test-ns" const ( - testControllerImage = "ghcr.io/datasance/controller:3.8.0-rc.4" + 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" diff --git a/internal/resource/edgelet_golden_test.go b/internal/resource/edgelet_golden_test.go index 9a7fc5037..bc5334ffd 100644 --- a/internal/resource/edgelet_golden_test.go +++ b/internal/resource/edgelet_golden_test.go @@ -67,8 +67,8 @@ func TestGoldenUnmarshalRemoteAgentPackageRegistry(t *testing.T) { agent, err := UnmarshallRemoteAgent(raw) require.NoError(t, err) require.True(t, agent.Airgap) - require.Equal(t, "1.0.0-rc.3", agent.Package.Version) - require.Equal(t, "ghcr.io/datasance/edgelet:1.0.0-rc.3", agent.Package.Container.Image) + 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) @@ -76,7 +76,7 @@ func TestGoldenUnmarshalRemoteAgentPackageRegistry(t *testing.T) { 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.3"}, agent.Scripts.Install.Args) + require.Equal(t, []string{"1.0.0-rc.6"}, agent.Scripts.Install.Args) require.Equal(t, "uninstall.sh", agent.Scripts.Uninstall.Name) } @@ -85,8 +85,8 @@ func TestGoldenUnmarshalLocalAgent(t *testing.T) { agent, err := UnmarshallLocalAgent(raw) require.NoError(t, err) require.Equal(t, "local", agent.Name) - require.Equal(t, "1.0.0-rc.3", agent.Package.Version) - require.Equal(t, "ghcr.io/datasance/edgelet:1.0.0-rc.3", agent.Package.Container.Image) + 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()) diff --git a/internal/resource/testdata/edgelet/local-agent-spec.yaml b/internal/resource/testdata/edgelet/local-agent-spec.yaml index cafbbc1d4..3f80c1542 100644 --- a/internal/resource/testdata/edgelet/local-agent-spec.yaml +++ b/internal/resource/testdata/edgelet/local-agent-spec.yaml @@ -1,7 +1,7 @@ package: - version: 1.0.0-rc.3 + version: 1.0.0-rc.6 container: - image: ghcr.io/datasance/edgelet:1.0.0-rc.3 + image: ghcr.io/datasance/edgelet:1.0.0-rc.6 config: description: edgelet running on device host: 30.40.50.6 diff --git a/internal/resource/testdata/edgelet/remote-agent-package-registry.yaml b/internal/resource/testdata/edgelet/remote-agent-package-registry.yaml index b3e6b0c0a..03f5409b2 100644 --- a/internal/resource/testdata/edgelet/remote-agent-package-registry.yaml +++ b/internal/resource/testdata/edgelet/remote-agent-package-registry.yaml @@ -5,9 +5,9 @@ ssh: port: 22 airgap: true package: - version: 1.0.0-rc.3 + version: 1.0.0-rc.6 container: - image: ghcr.io/datasance/edgelet:1.0.0-rc.3 + image: ghcr.io/datasance/edgelet:1.0.0-rc.6 registry: ghcr.io username: foo password: bar @@ -18,7 +18,7 @@ scripts: install: entrypoint: install.sh args: - - 1.0.0-rc.3 + - 1.0.0-rc.6 uninstall: entrypoint: uninstall.sh config: diff --git a/internal/resource/testdata/k8s/controlplane-datasance.yaml b/internal/resource/testdata/k8s/controlplane-datasance.yaml index 1c88b1929..da014e636 100644 --- a/internal/resource/testdata/k8s/controlplane-datasance.yaml +++ b/internal/resource/testdata/k8s/controlplane-datasance.yaml @@ -26,7 +26,7 @@ events: captureIpAddress: true images: operator: ghcr.io/datasance/operator:3.8.0-rc.1 - controller: ghcr.io/datasance/controller:3.8.0-rc.4 + 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: diff --git a/internal/resource/testdata/k8s/controlplane-iofog.yaml b/internal/resource/testdata/k8s/controlplane-iofog.yaml index e69d78526..e006bf4dc 100644 --- a/internal/resource/testdata/k8s/controlplane-iofog.yaml +++ b/internal/resource/testdata/k8s/controlplane-iofog.yaml @@ -26,7 +26,7 @@ events: captureIpAddress: true images: operator: ghcr.io/eclipse-iofog/operator:3.8.0-rc.1 - controller: ghcr.io/eclipse-iofog/controller:3.8.0-rc.4 + 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: diff --git a/internal/resource/testdata/k8s/controlplane.yaml b/internal/resource/testdata/k8s/controlplane.yaml index 8596d22cc..409669969 100644 --- a/internal/resource/testdata/k8s/controlplane.yaml +++ b/internal/resource/testdata/k8s/controlplane.yaml @@ -53,7 +53,7 @@ spec: # id: controller # secret: "" images: - controller: ghcr.io/datasance/controller:3.8.0-rc.4 + 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: diff --git a/internal/resource/testdata/local/controlplane.yaml b/internal/resource/testdata/local/controlplane.yaml index 5a410194a..81c202a0c 100644 --- a/internal/resource/testdata/local/controlplane.yaml +++ b/internal/resource/testdata/local/controlplane.yaml @@ -13,7 +13,7 @@ spec: consoleUrl: http://192.168.1.6 logLevel: info package: - image: ghcr.io/datasance/controller:3.8.0-rc.4 + image: ghcr.io/datasance/controller:3.8.0-rc.5 auth: mode: embedded insecureAllowHttp: true diff --git a/internal/resource/testdata/remote/controlplane.yaml b/internal/resource/testdata/remote/controlplane.yaml index dbbf03054..63cdd6331 100644 --- a/internal/resource/testdata/remote/controlplane.yaml +++ b/internal/resource/testdata/remote/controlplane.yaml @@ -15,7 +15,7 @@ spec: consoleUrl: https://192.168.105.2:80 logLevel: debug package: - image: ghcr.io/datasance/controller:3.8.0-rc.4 + image: ghcr.io/datasance/controller:3.8.0-rc.5 auth: mode: embedded insecureAllowHttp: false @@ -64,14 +64,14 @@ spec: ssh: user: emirhan keyFile: /Users/emirhan/.lima/_config/user - port: 61954 + port: 62787 systemAgent: config: host: 192.168.105.2 arch: arm64 containerEngine: edgelet deploymentType: native - networkInterface: lima0 + networkInterface: dynamic # - name: remote-2 # host: 192.168.139.148 # ssh: diff --git a/internal/resource/testdata/remote/remote.yaml b/internal/resource/testdata/remote/remote.yaml index f32cbc790..4e2aa7c9d 100644 --- a/internal/resource/testdata/remote/remote.yaml +++ b/internal/resource/testdata/remote/remote.yaml @@ -8,14 +8,14 @@ spec: ssh: user: emirhan keyFile: /Users/emirhan/.lima/_config/user - port: 57875 + port: 63025 airgap: true # package: # container: - # image: ghcr.io/datasance/edgelet:1.0.0-rc.3 + # image: ghcr.io/datasance/edgelet:1.0.0-rc.6 config: host: 192.168.105.3 - networkInterface: lima0 + networkInterface: dynamic logLevel: INFO deploymentType: native containerEngine: edgelet diff --git a/internal/util/client/client_local_cp_sync_test.go b/internal/util/client/client_local_cp_sync_test.go index 196667bfd..f4f9c7a91 100644 --- a/internal/util/client/client_local_cp_sync_test.go +++ b/internal/util/client/client_local_cp_sync_test.go @@ -47,7 +47,7 @@ func TestMergeRemoteAgentFromBackendPreservesSSHHostForLocalCPAgents(t *testing. Host: sshHost, SSH: rsc.SSH{User: "ubuntu", Port: 22, KeyFile: "/tmp/id_ed25519"}, Package: rsc.Package{ - Version: "v1.0.0-rc.5", + Version: "v1.0.0-rc.6", }, } diff --git a/internal/util/client/client_sync_test.go b/internal/util/client/client_sync_test.go index a7bf83f90..55008cb0c 100644 --- a/internal/util/client/client_sync_test.go +++ b/internal/util/client/client_sync_test.go @@ -18,7 +18,7 @@ func TestMergeRemoteAgentFromBackendPreservesSSHHost(t *testing.T) { SSH: rsc.SSH{User: "ubuntu2", Port: 32222, KeyFile: "/tmp/id_ed25519"}, Airgap: true, Package: rsc.Package{ - Version: "v1.0.0-rc.5", + Version: "v1.0.0-rc.6", }, } @@ -34,7 +34,7 @@ func TestMergeRemoteAgentFromBackendPreservesSSHHost(t *testing.T) { 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.5" { + 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" { diff --git a/pkg/iofog/install/edgelet_procedures_test.go b/pkg/iofog/install/edgelet_procedures_test.go index 611042f17..105b9ec3d 100644 --- a/pkg/iofog/install/edgelet_procedures_test.go +++ b/pkg/iofog/install/edgelet_procedures_test.go @@ -11,7 +11,7 @@ import ( func TestDefaultEdgeletProceduresScripts(t *testing.T) { util.SetEdgeletReleaseBaseForTest("https://github.com/Datasance/edgelet/releases/download") - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.5") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") t.Cleanup(func() { util.ResetEdgeletReleaseBaseForTest() util.ResetEdgeletBinaryVersionForTest() @@ -39,7 +39,7 @@ func TestDefaultEdgeletProceduresScripts(t *testing.T) { 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.5") { + 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") { @@ -132,7 +132,7 @@ func TestInstallDepsSkipMatrix(t *testing.T) { func TestBootstrapCommandGeneration(t *testing.T) { util.SetEdgeletReleaseBaseForTest("https://example.com/download") - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.5") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") cfg := EdgeletInstallConfig{HostOS: "linux", Arch: "amd64", ContainerEngine: "docker"} procs, err := newDefaultEdgeletProcedures(EdgeletScriptStageDir, cfg) diff --git a/pkg/iofog/install/edgelet_remote_test.go b/pkg/iofog/install/edgelet_remote_test.go index 3c0e8d533..7835969a6 100644 --- a/pkg/iofog/install/edgelet_remote_test.go +++ b/pkg/iofog/install/edgelet_remote_test.go @@ -10,7 +10,7 @@ import ( func TestRemoteEdgeletBootstrapUsesMockedSSH(t *testing.T) { util.SetEdgeletReleaseBaseForTest("https://example.com/download") - util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.5") + util.SetEdgeletBinaryVersionForTest("v1.0.0-rc.6") t.Cleanup(func() { util.ResetEdgeletReleaseBaseForTest() util.ResetEdgeletBinaryVersionForTest() diff --git a/pkg/iofog/install/procedures_test.go b/pkg/iofog/install/procedures_test.go index d5b7d174d..f0a38dfb8 100644 --- a/pkg/iofog/install/procedures_test.go +++ b/pkg/iofog/install/procedures_test.go @@ -5,10 +5,10 @@ import "testing" func TestEntrypointGetCommandQuotesInstallArgs(t *testing.T) { ep := Entrypoint{ destPath: "/tmp/edgelet-scripts/install.sh", - Args: []string{"--version=v1.0.0-rc.5", "--arch=arm64", "--skip-start"}, + 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.5' '--arch=arm64' '--skip-start'" + 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/versions.mk b/versions.mk index 72511e208..24fd51557 100644 --- a/versions.mk +++ b/versions.mk @@ -1,6 +1,6 @@ OPERATOR_VERSION ?= 3.8.0-rc.1 -CONTROLLER_VERSION ?= 3.8.0-rc.4 +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.5 -EDGELET_IMAGE_TAG ?= 1.0.0-rc.3 +EDGELET_BINARY_VERSION ?= v1.0.0-rc.6 +EDGELET_IMAGE_TAG ?= 1.0.0-rc.6 From f3e3b4e02f549ab5496535c8ccb986f37c1d1b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 27 Jun 2026 17:18:29 +0300 Subject: [PATCH 57/63] Validate edgelet download URLs against the configured release base path. --- pkg/util/edgelet_binary.go | 2 +- pkg/util/edgelet_binary_test.go | 43 ++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/pkg/util/edgelet_binary.go b/pkg/util/edgelet_binary.go index 53511e747..0b2d9ead4 100644 --- a/pkg/util/edgelet_binary.go +++ b/pkg/util/edgelet_binary.go @@ -138,7 +138,7 @@ func validateEdgeletDownloadURL(downloadURL string) error { } version := GetEdgeletBinaryVersion() - wantPrefix := "/" + version + "/" + wantPrefix := strings.TrimRight(base.Path, "/") + "/" + version + "/" if !strings.HasPrefix(download.Path, wantPrefix) { return fmt.Errorf("unexpected download path %q", download.Path) } diff --git a/pkg/util/edgelet_binary_test.go b/pkg/util/edgelet_binary_test.go index 507dfb831..54121ed08 100644 --- a/pkg/util/edgelet_binary_test.go +++ b/pkg/util/edgelet_binary_test.go @@ -45,17 +45,22 @@ func TestEdgeletBinaryArtifact(t *testing.T) { } func TestEdgeletBinaryURL(t *testing.T) { - edgeletReleaseBase = "https://github.com/Datasance/edgelet/releases/download" - edgeletBinaryVersion = "v1.0.0-rc.5" + 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.5/edgelet-linux-amd64" + 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) { @@ -78,9 +83,33 @@ func TestShouldSkipInstallDeps(t *testing.T) { } } +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.5/edgelet-linux-amd64" { + if r.URL.Path != "/v1.0.0-rc.6/edgelet-linux-amd64" { http.NotFound(w, r) return } @@ -88,8 +117,10 @@ func TestDownloadEdgeletBinary(t *testing.T) { })) defer server.Close() - edgeletReleaseBase = server.URL - edgeletBinaryVersion = "v1.0.0-rc.5" + 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") From 7333749bcf12bddc01f241b1ff93936d94a6830a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 27 Jun 2026 17:18:35 +0300 Subject: [PATCH 58/63] Resolve EdgeOps Console URLs from control plane spec and backfill on deploy. --- internal/cmd/view.go | 47 +--- internal/connect/controlplane/k8s/k8s.go | 6 + internal/deploy/controlplane/k8s/execute.go | 4 +- internal/deploy/controlplane/local/execute.go | 3 + .../deploy/controlplane/remote/execute.go | 4 + internal/resource/console_url.go | 212 ++++++++++++++++++ internal/resource/console_url_test.go | 191 ++++++++++++++++ 7 files changed, 426 insertions(+), 41 deletions(-) create mode 100644 internal/resource/console_url.go create mode 100644 internal/resource/console_url_test.go diff --git a/internal/cmd/view.go b/internal/cmd/view.go index 4752f91af..02c4cbebf 100644 --- a/internal/cmd/view.go +++ b/internal/cmd/view.go @@ -2,13 +2,10 @@ 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" @@ -17,54 +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() util.Check(err) - cpEndpoint, err := cp.GetEndpoint() + 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/connect/controlplane/k8s/k8s.go b/internal/connect/controlplane/k8s/k8s.go index 67bcb8b46..1d46bfd42 100644 --- a/internal/connect/controlplane/k8s/k8s.go +++ b/internal/connect/controlplane/k8s/k8s.go @@ -101,6 +101,12 @@ 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 + } ns.SetControlPlane(exe.controlPlane) return config.Flush() diff --git a/internal/deploy/controlplane/k8s/execute.go b/internal/deploy/controlplane/k8s/execute.go index 95cba771e..581e72e29 100644 --- a/internal/deploy/controlplane/k8s/execute.go +++ b/internal/deploy/controlplane/k8s/execute.go @@ -130,8 +130,8 @@ func (exe *kubernetesControlPlaneExecutor) executeInstall() (err error) { if exe.controlPlane.Controller.PublicUrl == "" { exe.controlPlane.Controller.PublicUrl = endpoint } - if exe.controlPlane.Controller.ConsoleUrl == "" { - exe.controlPlane.Controller.ConsoleUrl = endpoint + if err := rsc.BackfillConsoleURL(exe.controlPlane); err != nil { + return err } return err diff --git a/internal/deploy/controlplane/local/execute.go b/internal/deploy/controlplane/local/execute.go index 67a0752e0..9d071bef7 100644 --- a/internal/deploy/controlplane/local/execute.go +++ b/internal/deploy/controlplane/local/execute.go @@ -57,6 +57,9 @@ func (exe localControlPlaneExecutor) Execute() (err error) { if err := persistLocalControllerStub(exe.controlPlane, exe.name, endpoint); err != nil { return err } + if err := rsc.BackfillConsoleURL(exe.controlPlane); err != nil { + return err + } if err := trust.WaitForControllerAPI(context.Background(), exe.namespace, endpoint); err != nil { return err diff --git a/internal/deploy/controlplane/remote/execute.go b/internal/deploy/controlplane/remote/execute.go index eb3b67c6b..8eb284ab5 100644 --- a/internal/deploy/controlplane/remote/execute.go +++ b/internal/deploy/controlplane/remote/execute.go @@ -113,6 +113,10 @@ func (exe remoteControlPlaneExecutor) Execute() (err error) { return err } + if err := rsc.BackfillConsoleURL(exe.controlPlane); err != nil { + return err + } + ns.SetControlPlane(exe.controlPlane) return config.Flush() } 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) + } +} From 03767a8cc57d6bcbf6e1c0558dba6bb3f3d4d225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 27 Jun 2026 17:18:42 +0300 Subject: [PATCH 59/63] Route exec through SDK dial sessions and remove attach/detach exec microservice. --- go.mod | 8 +- go.sum | 4 +- internal/attach/exec/agent/execute.go | 2 +- internal/attach/exec/microservice/execute.go | 82 ----- internal/cmd/attach_exec.go | 47 +-- internal/cmd/detach_exec.go | 47 +-- internal/cmd/exec_agent.go | 15 +- internal/cmd/exec_microservice.go | 4 +- internal/detach/exec/agent/execute.go | 2 +- internal/detach/exec/microservice/execute.go | 82 ----- internal/exec/agent.go | 114 +----- internal/exec/debug.go | 147 ++++++++ internal/exec/debug_test.go | 48 +++ internal/exec/factory.go | 9 +- internal/exec/interactive_terminal.go | 160 ++++++++ internal/exec/interactive_terminal_unix.go | 42 +++ internal/exec/interactive_terminal_windows.go | 42 +++ internal/exec/lookup.go | 29 ++ internal/exec/microservice.go | 108 +----- internal/exec/session.go | 68 ++++ internal/exec/shell_exit.go | 69 ++++ internal/exec/shell_exit_test.go | 44 +++ internal/exec/utils.go | 182 +++------- internal/util/terminal/terminal.go | 343 ------------------ internal/util/terminal/terminal_windows.go | 234 ------------ internal/util/websocket/client.go | 341 ----------------- internal/util/websocket/constants.go | 25 -- internal/util/websocket/message.go | 97 ----- internal/util/websocket/session.go | 90 ----- 29 files changed, 768 insertions(+), 1717 deletions(-) delete mode 100644 internal/attach/exec/microservice/execute.go delete mode 100644 internal/detach/exec/microservice/execute.go create mode 100644 internal/exec/debug.go create mode 100644 internal/exec/debug_test.go create mode 100644 internal/exec/interactive_terminal.go create mode 100644 internal/exec/interactive_terminal_unix.go create mode 100644 internal/exec/interactive_terminal_windows.go create mode 100644 internal/exec/lookup.go create mode 100644 internal/exec/session.go create mode 100644 internal/exec/shell_exit.go create mode 100644 internal/exec/shell_exit_test.go delete mode 100644 internal/util/terminal/terminal.go delete mode 100644 internal/util/terminal/terminal_windows.go delete mode 100644 internal/util/websocket/client.go delete mode 100644 internal/util/websocket/constants.go delete mode 100644 internal/util/websocket/message.go delete mode 100644 internal/util/websocket/session.go diff --git a/go.mod b/go.mod index ed9efcf19..080c0209e 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,8 @@ go 1.26.4 require ( github.com/briandowns/spinner v1.23.1 - github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.7 + 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/gorilla/websocket v1.5.3 github.com/mitchellh/go-homedir v1.1.0 github.com/moby/moby/api v1.55.0 github.com/moby/moby/client v0.5.0 @@ -16,10 +15,8 @@ require ( 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 go.podman.io/image/v5 v5.40.0 golang.org/x/crypto v0.53.0 - golang.org/x/sys v0.46.0 golang.org/x/term v0.44.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.32.1 @@ -73,6 +70,7 @@ require ( 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/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 @@ -113,6 +111,7 @@ require ( 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.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -125,6 +124,7 @@ require ( 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 diff --git a/go.sum b/go.sum index e32596b94..2da3f2927 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf 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.8.0-rc.7 h1:3Zctfg/w0hi9/psUoK7aL0ciUAbsUvCmcEn1pREP0kY= -github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.0-rc.7/go.mod h1:MU+YPxFRGFzBMboi1khtEGAoRwaF4yJX3KuBwFcfCKg= +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= diff --git a/internal/attach/exec/agent/execute.go b/internal/attach/exec/agent/execute.go index 6d58f24f8..d61cbad07 100644 --- a/internal/attach/exec/agent/execute.go +++ b/internal/attach/exec/agent/execute.go @@ -34,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) diff --git a/internal/attach/exec/microservice/execute.go b/internal/attach/exec/microservice/execute.go deleted file mode 100644 index 2e3061cf6..000000000 --- a/internal/attach/exec/microservice/execute.go +++ /dev/null @@ -1,82 +0,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/cmd/attach_exec.go b/internal/cmd/attach_exec.go index 43f8d2df1..0280fc3db 100644 --- a/internal/cmd/attach_exec.go +++ b/internal/cmd/attach_exec.go @@ -4,44 +4,16 @@ 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: ex(`%[1]s 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.`, + 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) { @@ -53,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) }, } @@ -69,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: ex(`%[1]s 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/detach_exec.go b/internal/cmd/detach_exec.go index b32fb281f..7f4fa9c25 100644 --- a/internal/cmd/detach_exec.go +++ b/internal/cmd/detach_exec.go @@ -4,44 +4,16 @@ 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: ex(`%[1]s 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.`, + 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) { @@ -50,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) }, } @@ -66,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: ex(`%[1]s 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/exec_agent.go b/internal/cmd/exec_agent.go index 9ddafe1e2..bbf47f021 100644 --- a/internal/cmd/exec_agent.go +++ b/internal/cmd/exec_agent.go @@ -12,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: ex(`%[1]s 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 e11a6b295..3473a3aa4 100644 --- a/internal/cmd/exec_microservice.go +++ b/internal/cmd/exec_microservice.go @@ -13,8 +13,8 @@ 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.`, + 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) { diff --git a/internal/detach/exec/agent/execute.go b/internal/detach/exec/agent/execute.go index 1b0e4c5b8..e46d45dc0 100644 --- a/internal/detach/exec/agent/execute.go +++ b/internal/detach/exec/agent/execute.go @@ -31,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) diff --git a/internal/detach/exec/microservice/execute.go b/internal/detach/exec/microservice/execute.go deleted file mode 100644 index 1d18dbfb3..000000000 --- a/internal/detach/exec/microservice/execute.go +++ /dev/null @@ -1,82 +0,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/exec/agent.go b/internal/exec/agent.go index 31dc38298..181774f53 100644 --- a/internal/exec/agent.go +++ b/internal/exec/agent.go @@ -1,31 +1,21 @@ package exec import ( - "context" - "fmt" - "net/http" - "strings" - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/internal/trust" - 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 { @@ -33,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 { - return fmt.Errorf("failed to get Agent by name: %w", err) - } - - 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() - tlsCfg := trust.TLSConfigForController(context.Background(), exe.namespace, controllerURL) - // 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, tlsCfg); 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 55c98863a..866fc2140 100644 --- a/internal/exec/factory.go +++ b/internal/exec/factory.go @@ -8,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) { @@ -18,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 a62221351..7dc76caf8 100644 --- a/internal/exec/microservice.go +++ b/internal/exec/microservice.go @@ -1,31 +1,19 @@ package exec import ( - "context" - "fmt" - "net/http" - "strings" - "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" - "github.com/eclipse-iofog/iofogctl/internal/trust" - 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 { @@ -33,88 +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() - tlsCfg := trust.TLSConfigForController(context.Background(), exe.namespace, controllerURL) - // 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, tlsCfg); 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 2f9d511e6..b8816391e 100644 --- a/internal/exec/utils.go +++ b/internal/exec/utils.go @@ -1,168 +1,100 @@ 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.TrimSuffix(strings.TrimSpace(errStr[idx+7:]), ".") - 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.TrimSuffix(strings.TrimSpace(errStr[idx+18:]), ".") - 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.TrimSuffix(strings.TrimSpace(afterClose[colonIdx+1:]), ".") - return reason - } - - // Return the whole thing if it looks like a reason - if len(afterClose) > 0 && !strings.Contains(afterClose, "websocket") { - return afterClose - } + 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 "" } diff --git a/internal/util/terminal/terminal.go b/internal/util/terminal/terminal.go deleted file mode 100644 index 0bd8323ee..000000000 --- a/internal/util/terminal/terminal.go +++ /dev/null @@ -1,343 +0,0 @@ -//go:build !windows -// +build !windows - -package terminal - -import ( - "context" - "fmt" - "os" - "os/signal" - "sync" - "time" - - ws "github.com/eclipse-iofog/iofogctl/internal/util/websocket" - "github.com/eclipse-iofog/iofogctl/pkg/util" - "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() - util.WriteStdout(data) -} - -// 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() - util.Log(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() - util.Log(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 - util.WriteStdout([]byte("\r\x1b[K")) - - // Redraw the prompt and input - if t.prompt != "" { - util.WriteStdoutString(t.prompt) - } - util.WriteStdout([]byte(string(t.inputBuffer))) - - // Move cursor to end of input - t.cursorPos = len(t.inputBuffer) - util.WriteStdout([]byte("\r")) // Move to start of line first - if t.prompt != "" { - util.WriteStdoutString(t.prompt) // Move past prompt - } - if t.cursorPos > 0 { - fmt.Fprintf(os.Stdout, "\x1b[%dC", t.cursorPos) // Move to cursor position - } -} - -// 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 { - util.Log(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: %w", 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 addac0777..000000000 --- a/internal/util/terminal/terminal_windows.go +++ /dev/null @@ -1,234 +0,0 @@ -//go:build windows -// +build windows - -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/websocket/client.go b/internal/util/websocket/client.go deleted file mode 100644 index 4aa8d3f27..000000000 --- a/internal/util/websocket/client.go +++ /dev/null @@ -1,341 +0,0 @@ -package websocket - -import ( - "crypto/tls" - "errors" - "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. -// tlsCfg is required for wss:// URLs; pass nil for ws://. -func (c *Client) Connect(url string, headers http.Header, tlsCfg *tls.Config) error { - dialer := websocket.Dialer{ - HandshakeTimeout: 45 * time.Second, - ReadBufferSize: 1024, - WriteBufferSize: 1024, - } - if tlsCfg != nil { - dialer.TLSClientConfig = tlsCfg - } - - 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) { - closeErr := &websocket.CloseError{} - if errors.As(err, &closeErr) { - c.errMutex.Lock() - c.err = util.NewError(fmt.Sprintf("connection closed by server (code: %d, reason: %s)", closeErr.Code, closeErr.Text)) - c.errMutex.Unlock() - util.Log(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() - util.Log(c.Close) - return c.err - } - c.errMutex.Lock() - c.err = util.NewError(fmt.Sprintf("failed to establish WebSocket connection: %v", err)) - c.errMutex.Unlock() - util.Log(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() - util.Log(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 - util.Log(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() - util.Log(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 - util.Log(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 - closeErr := &websocket.CloseError{} - if errors.As(err, &closeErr) { - 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() - util.Log(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() - util.Log(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 - closeErr := &websocket.CloseError{} - if errors.As(err, &closeErr) { - 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 23620af55..000000000 --- a/internal/util/websocket/constants.go +++ /dev/null @@ -1,25 +0,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 f09a7d134..000000000 --- a/internal/util/websocket/message.go +++ /dev/null @@ -1,97 +0,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: %w", 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 a2865dfe7..000000000 --- a/internal/util/websocket/session.go +++ /dev/null @@ -1,90 +0,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) -} From 7beb610ce5f2526cbcdc2d6eeb0f16b05c6a931b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 27 Jun 2026 17:18:47 +0300 Subject: [PATCH 60/63] Stream remote logs through SDK log sessions and align --tail max with controller. --- internal/cmd/logs.go | 2 +- internal/logs/agent.go | 60 +---------- internal/logs/config.go | 20 +++- internal/logs/microservice.go | 64 ++---------- internal/logs/stream.go | 182 +++++++++++++--------------------- 5 files changed, 95 insertions(+), 233 deletions(-) diff --git a/internal/cmd/logs.go b/internal/cmd/logs.go index 7c42a84c7..ba8e277db 100644 --- a/internal/cmd/logs.go +++ b/internal/cmd/logs.go @@ -55,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/logs/agent.go b/internal/logs/agent.go index 8d9c100c9..fd6953afd 100644 --- a/internal/logs/agent.go +++ b/internal/logs/agent.go @@ -1,17 +1,12 @@ package logs import ( - "context" "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" - "github.com/eclipse-iofog/iofogctl/internal/trust" 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" ) @@ -66,71 +61,24 @@ 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() - tlsCfg := trust.TLSConfigForController(context.Background(), exe.namespace, controllerURL) - // 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, tlsCfg); 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 diff --git a/internal/logs/config.go b/internal/logs/config.go index 28b0a59f1..d571ff65f 100644 --- a/internal/logs/config.go +++ b/internal/logs/config.go @@ -5,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) @@ -27,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 @@ -71,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) diff --git a/internal/logs/microservice.go b/internal/logs/microservice.go index cac021794..e84948fe0 100644 --- a/internal/logs/microservice.go +++ b/internal/logs/microservice.go @@ -1,17 +1,12 @@ package logs import ( - "context" - "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" - "github.com/eclipse-iofog/iofogctl/internal/trust" 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" ) @@ -62,68 +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() - tlsCfg := trust.TLSConfigForController(context.Background(), ms.namespace, controllerURL) - // 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, tlsCfg); 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/stream.go b/internal/logs/stream.go index 9191e7952..1d1fc06a2 100644 --- a/internal/logs/stream.go +++ b/internal/logs/stream.go @@ -1,141 +1,99 @@ package logs import ( - "context" + "errors" "fmt" "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, + for { + frame, err := session.Read() + if err != nil { + return util.NewError(formatLogError(err)) + } + if frame == nil { + return nil + } + + 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) + } } } -func (ls *LogStream) writeToStdout(data []byte) { - ls.stdoutMutex.Lock() - defer ls.stdoutMutex.Unlock() +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) } -func (ls *LogStream) cleanup() { - ls.cleanupOnce.Do(func() { - if ls.wsClient != nil { - util.Log(ls.wsClient.Close) - } - }) -} - -// 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 - } - - // 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" } @@ -151,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) } From 5c269b1f3434811b7ab36801f030780c21babc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 27 Jun 2026 17:18:52 +0300 Subject: [PATCH 61/63] Regenerate potctl and iofogctl CLI reference documentation. --- docs/iofogctl_md/iofogctl.md | 2 +- docs/iofogctl_md/iofogctl_attach.md | 2 +- docs/iofogctl_md/iofogctl_attach_exec.md | 9 ++--- .../iofogctl_md/iofogctl_attach_exec_agent.md | 6 +-- .../iofogctl_attach_exec_microservice.md | 37 ------------------- docs/iofogctl_md/iofogctl_detach.md | 2 +- docs/iofogctl_md/iofogctl_detach_exec.md | 9 ++--- .../iofogctl_md/iofogctl_detach_exec_agent.md | 6 +-- .../iofogctl_detach_exec_microservice.md | 37 ------------------- docs/iofogctl_md/iofogctl_exec.md | 4 +- docs/iofogctl_md/iofogctl_exec_agent.md | 7 ++-- .../iofogctl_md/iofogctl_exec_microservice.md | 4 +- docs/iofogctl_md/iofogctl_logs.md | 2 +- docs/iofogctl_md/iofogctl_view.md | 2 +- docs/potctl_md/potctl.md | 2 +- docs/potctl_md/potctl_attach.md | 2 +- docs/potctl_md/potctl_attach_exec.md | 9 ++--- docs/potctl_md/potctl_attach_exec_agent.md | 6 +-- .../potctl_attach_exec_microservice.md | 37 ------------------- docs/potctl_md/potctl_detach.md | 2 +- docs/potctl_md/potctl_detach_exec.md | 9 ++--- docs/potctl_md/potctl_detach_exec_agent.md | 6 +-- .../potctl_detach_exec_microservice.md | 37 ------------------- docs/potctl_md/potctl_exec.md | 4 +- docs/potctl_md/potctl_exec_agent.md | 7 ++-- docs/potctl_md/potctl_exec_microservice.md | 4 +- docs/potctl_md/potctl_logs.md | 2 +- docs/potctl_md/potctl_view.md | 2 +- 28 files changed, 54 insertions(+), 204 deletions(-) delete mode 100644 docs/iofogctl_md/iofogctl_attach_exec_microservice.md delete mode 100644 docs/iofogctl_md/iofogctl_detach_exec_microservice.md delete mode 100644 docs/potctl_md/potctl_attach_exec_microservice.md delete mode 100644 docs/potctl_md/potctl_detach_exec_microservice.md diff --git a/docs/iofogctl_md/iofogctl.md b/docs/iofogctl_md/iofogctl.md index 1d44a4cc9..cd35fc9b3 100644 --- a/docs/iofogctl_md/iofogctl.md +++ b/docs/iofogctl_md/iofogctl.md @@ -40,6 +40,6 @@ iofogctl [flags] * [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/iofogctl_md/iofogctl_attach.md b/docs/iofogctl_md/iofogctl_attach.md index b3c77d36b..64737aa91 100644 --- a/docs/iofogctl_md/iofogctl_attach.md +++ b/docs/iofogctl_md/iofogctl_attach.md @@ -30,7 +30,7 @@ iofogctl attach * [iofogctl](iofogctl.md) - * [iofogctl attach agent](iofogctl_attach_agent.md) - Attach an Agent to an existing Namespace -* [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/iofogctl_md/iofogctl_attach_exec.md b/docs/iofogctl_md/iofogctl_attach_exec.md index 7bc2bcb94..eaecd9252 100644 --- a/docs/iofogctl_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/iofogctl_md/iofogctl_attach_exec_agent.md b/docs/iofogctl_md/iofogctl_attach_exec_agent.md index df6d88801..e745247dc 100644 --- a/docs/iofogctl_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/iofogctl_md/iofogctl_attach_exec_microservice.md b/docs/iofogctl_md/iofogctl_attach_exec_microservice.md deleted file mode 100644 index 908d81b3e..000000000 --- a/docs/iofogctl_md/iofogctl_attach_exec_microservice.md +++ /dev/null @@ -1,37 +0,0 @@ -## iofogctl attach exec microservice - -Attach an Exec Session to a Microservice - -### Synopsis - -Attach an Exec Session to an existing Microservice. - -``` -iofogctl attach exec microservice NAME [flags] -``` - -### Examples - -``` -iofogctl attach 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 iofogctl -``` - -### SEE ALSO - -* [iofogctl attach exec](iofogctl_attach_exec.md) - Attach an Exec Session to a resource - - diff --git a/docs/iofogctl_md/iofogctl_detach.md b/docs/iofogctl_md/iofogctl_detach.md index 5d1ba6968..0e367f78c 100644 --- a/docs/iofogctl_md/iofogctl_detach.md +++ b/docs/iofogctl_md/iofogctl_detach.md @@ -30,7 +30,7 @@ iofogctl detach * [iofogctl](iofogctl.md) - * [iofogctl detach agent](iofogctl_detach_agent.md) - Detaches 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/iofogctl_md/iofogctl_detach_exec.md b/docs/iofogctl_md/iofogctl_detach_exec.md index 6c2843108..61d6e8e9d 100644 --- a/docs/iofogctl_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/iofogctl_md/iofogctl_detach_exec_agent.md b/docs/iofogctl_md/iofogctl_detach_exec_agent.md index 46af5f8ce..29a4dbcfc 100644 --- a/docs/iofogctl_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/iofogctl_md/iofogctl_detach_exec_microservice.md b/docs/iofogctl_md/iofogctl_detach_exec_microservice.md deleted file mode 100644 index 115f7ce91..000000000 --- a/docs/iofogctl_md/iofogctl_detach_exec_microservice.md +++ /dev/null @@ -1,37 +0,0 @@ -## iofogctl detach exec microservice - -Detach an Exec Session to a Microservice - -### Synopsis - -Detach an Exec Session to an existing Microservice. - -``` -iofogctl detach exec microservice NAME [flags] -``` - -### Examples - -``` -iofogctl detach 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 iofogctl -``` - -### SEE ALSO - -* [iofogctl detach exec](iofogctl_detach_exec.md) - Detach an Exec Session to a resource - - diff --git a/docs/iofogctl_md/iofogctl_exec.md b/docs/iofogctl_md/iofogctl_exec.md index 1d8cc8ae4..3a0a304c1 100644 --- a/docs/iofogctl_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/iofogctl_md/iofogctl_exec_agent.md b/docs/iofogctl_md/iofogctl_exec_agent.md index 216235e90..832517a4d 100644 --- a/docs/iofogctl_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/iofogctl_md/iofogctl_exec_microservice.md b/docs/iofogctl_md/iofogctl_exec_microservice.md index 92309f54d..f9b93e29f 100644 --- a/docs/iofogctl_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/iofogctl_md/iofogctl_logs.md b/docs/iofogctl_md/iofogctl_logs.md index b35e47370..96e799cf1 100644 --- a/docs/iofogctl_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/iofogctl_md/iofogctl_view.md b/docs/iofogctl_md/iofogctl_view.md index 3cbf27c2c..1b14b1302 100644 --- a/docs/iofogctl_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/potctl_md/potctl.md b/docs/potctl_md/potctl.md index a75442e3c..d5f5f326f 100644 --- a/docs/potctl_md/potctl.md +++ b/docs/potctl_md/potctl.md @@ -40,6 +40,6 @@ potctl [flags] * [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 ECN Viewer +* [potctl view](potctl_view.md) - Open EdgeOps Console diff --git a/docs/potctl_md/potctl_attach.md b/docs/potctl_md/potctl_attach.md index 9221a18cc..3dc8ad7dd 100644 --- a/docs/potctl_md/potctl_attach.md +++ b/docs/potctl_md/potctl_attach.md @@ -30,7 +30,7 @@ potctl attach * [potctl](potctl.md) - * [potctl attach agent](potctl_attach_agent.md) - Attach an Agent to an existing Namespace -* [potctl attach exec](potctl_attach_exec.md) - Attach an Exec Session to a resource +* [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_exec.md b/docs/potctl_md/potctl_attach_exec.md index 73beb2f76..358316a1b 100644 --- a/docs/potctl_md/potctl_attach_exec.md +++ b/docs/potctl_md/potctl_attach_exec.md @@ -1,15 +1,15 @@ ## potctl 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 ``` -potctl attach exec microservice AppName/MicroserviceName +potctl attach exec agent AgentName ``` ### Options @@ -29,7 +29,6 @@ potctl attach exec microservice AppName/MicroserviceName ### SEE ALSO * [potctl attach](potctl_attach.md) - Attach one ioFog resource to another -* [potctl attach exec agent](potctl_attach_exec_agent.md) - Attach an Exec Session to an Agent -* [potctl attach exec microservice](potctl_attach_exec_microservice.md) - Attach an Exec Session to a Microservice +* [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 index 4719d32d7..6fdfdef6a 100644 --- a/docs/potctl_md/potctl_attach_exec_agent.md +++ b/docs/potctl_md/potctl_attach_exec_agent.md @@ -1,10 +1,10 @@ ## potctl 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. ``` potctl attach exec agent NAME [DEBUG_IMAGE] [flags] @@ -32,6 +32,6 @@ potctl attach exec agent AgentName DebugImage ### SEE ALSO -* [potctl attach exec](potctl_attach_exec.md) - Attach an Exec Session to a resource +* [potctl attach exec](potctl_attach_exec.md) - Provision fog debug exec on an Agent diff --git a/docs/potctl_md/potctl_attach_exec_microservice.md b/docs/potctl_md/potctl_attach_exec_microservice.md deleted file mode 100644 index ecd46e8b0..000000000 --- a/docs/potctl_md/potctl_attach_exec_microservice.md +++ /dev/null @@ -1,37 +0,0 @@ -## potctl attach exec microservice - -Attach an Exec Session to a Microservice - -### Synopsis - -Attach an Exec Session to an existing Microservice. - -``` -potctl attach exec microservice NAME [flags] -``` - -### Examples - -``` -potctl attach 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 attach exec](potctl_attach_exec.md) - Attach an Exec Session to a resource - - diff --git a/docs/potctl_md/potctl_detach.md b/docs/potctl_md/potctl_detach.md index 302d215b1..0fcb2dac6 100644 --- a/docs/potctl_md/potctl_detach.md +++ b/docs/potctl_md/potctl_detach.md @@ -30,7 +30,7 @@ potctl detach * [potctl](potctl.md) - * [potctl detach agent](potctl_detach_agent.md) - Detaches an Agent -* [potctl detach exec](potctl_detach_exec.md) - Detach an Exec Session to a resource +* [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_exec.md b/docs/potctl_md/potctl_detach_exec.md index 93a0275c8..f11df409a 100644 --- a/docs/potctl_md/potctl_detach_exec.md +++ b/docs/potctl_md/potctl_detach_exec.md @@ -1,15 +1,15 @@ ## potctl 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 ``` -potctl detach exec microservice AppName/MicroserviceName +potctl detach exec agent AgentName ``` ### Options @@ -29,7 +29,6 @@ potctl detach exec microservice AppName/MicroserviceName ### SEE ALSO * [potctl detach](potctl_detach.md) - Detach one ioFog resource from another -* [potctl detach exec agent](potctl_detach_exec_agent.md) - Detach an Exec Session from an Agent -* [potctl detach exec microservice](potctl_detach_exec_microservice.md) - Detach an Exec Session to a Microservice +* [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 index 825eadf88..d14366fa9 100644 --- a/docs/potctl_md/potctl_detach_exec_agent.md +++ b/docs/potctl_md/potctl_detach_exec_agent.md @@ -1,10 +1,10 @@ ## potctl 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. ``` potctl detach exec agent NAME [flags] @@ -32,6 +32,6 @@ potctl detach exec agent AgentName ### SEE ALSO -* [potctl detach exec](potctl_detach_exec.md) - Detach an Exec Session to a resource +* [potctl detach exec](potctl_detach_exec.md) - Remove fog debug exec from an Agent diff --git a/docs/potctl_md/potctl_detach_exec_microservice.md b/docs/potctl_md/potctl_detach_exec_microservice.md deleted file mode 100644 index e975d847f..000000000 --- a/docs/potctl_md/potctl_detach_exec_microservice.md +++ /dev/null @@ -1,37 +0,0 @@ -## potctl detach exec microservice - -Detach an Exec Session to a Microservice - -### Synopsis - -Detach an Exec Session to an existing Microservice. - -``` -potctl detach exec microservice NAME [flags] -``` - -### Examples - -``` -potctl detach 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 detach exec](potctl_detach_exec.md) - Detach an Exec Session to a resource - - diff --git a/docs/potctl_md/potctl_exec.md b/docs/potctl_md/potctl_exec.md index ed5418fc0..d27a7313f 100644 --- a/docs/potctl_md/potctl_exec.md +++ b/docs/potctl_md/potctl_exec.md @@ -23,7 +23,7 @@ Connect to an Exec Session of a Microservice or Agent. ### SEE ALSO * [potctl](potctl.md) - -* [potctl exec agent](potctl_exec_agent.md) - Connect to an Exec Session of an Agent -* [potctl exec microservice](potctl_exec_microservice.md) - Connect to an Exec Session of a Microservice +* [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 index 4ed1e6271..f2264fe71 100644 --- a/docs/potctl_md/potctl_exec_agent.md +++ b/docs/potctl_md/potctl_exec_agent.md @@ -1,19 +1,20 @@ ## potctl 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. ``` -potctl exec agent AgentName [flags] +potctl exec agent AgentName [DEBUG_IMAGE] [flags] ``` ### Examples ``` potctl exec agent AgentName +potctl exec agent AgentName ghcr.io/org/debug:latest ``` ### Options diff --git a/docs/potctl_md/potctl_exec_microservice.md b/docs/potctl_md/potctl_exec_microservice.md index c4ca59a33..c35665ffd 100644 --- a/docs/potctl_md/potctl_exec_microservice.md +++ b/docs/potctl_md/potctl_exec_microservice.md @@ -1,10 +1,10 @@ ## potctl 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. ``` potctl exec microservice AppName/MsvcName [flags] diff --git a/docs/potctl_md/potctl_logs.md b/docs/potctl_md/potctl_logs.md index e132da18d..f13c62d3d 100644 --- a/docs/potctl_md/potctl_logs.md +++ b/docs/potctl_md/potctl_logs.md @@ -24,7 +24,7 @@ potctl 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/potctl_md/potctl_view.md b/docs/potctl_md/potctl_view.md index a48bf760e..f99dc0b3a 100644 --- a/docs/potctl_md/potctl_view.md +++ b/docs/potctl_md/potctl_view.md @@ -1,6 +1,6 @@ ## potctl view -Open ECN Viewer +Open EdgeOps Console ``` potctl view [flags] From 1c257456786d20403669b4024d7b597559eb3369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 27 Jun 2026 17:20:44 +0300 Subject: [PATCH 62/63] Document exec, logs, and console changes in the changelog. --- CHANGELOG.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16ed7ace2..9c9c729e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,9 @@ 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). -## [Unreleased] - ## [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 @@ -22,6 +21,8 @@ First greenfield v3.8 release candidate. Dual-flavor build (`potctl` / `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 @@ -30,6 +31,10 @@ First greenfield v3.8 release candidate. Dual-flavor build (`potctl` / `iofogctl - 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 @@ -39,6 +44,8 @@ First greenfield v3.8 release candidate. Dual-flavor build (`potctl` / `iofogctl - `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 @@ -53,8 +60,8 @@ First greenfield v3.8 release candidate. Dual-flavor build (`potctl` / `iofogctl | controller | `3.8.0-rc.4` | | router | `3.8.0-rc.1` | | nats | `2.14.2-rc.2` | -| edgelet binary | `v1.0.0-rc.5` | -| edgelet image | `ghcr.io//edgelet:1.0.0-rc.3` | +| edgelet binary | `v1.0.0-rc.6` | +| edgelet image | `ghcr.io//edgelet:1.0.0-rc.6` | ## Pre-3.8 history From f9a0e4605116848ded5b92a34be220725c300c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 27 Jun 2026 17:49:03 +0300 Subject: [PATCH 63/63] Derive release flavor from GitHub repo instead of a CI matrix. --- .github/workflows/release.yml | 20 +------------------- .goreleaser.yml | 6 +++--- CHANGELOG.md | 2 +- CONTRIBUTING | 20 +++++++++++--------- script/goreleaser-env-check.sh | 2 +- script/goreleaser-env.sh | 24 +++++++++++++++++++++--- script/goreleaser-release.sh | 1 - 7 files changed, 38 insertions(+), 37 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55e296f74..59802b4d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,25 +9,7 @@ env: GO_VERSION: '1.26.4' jobs: - release-matrix: - runs-on: ubuntu-latest - outputs: - flavors: ${{ steps.set.outputs.flavors }} - steps: - - id: set - run: | - if [ "${{ github.repository }}" = "Datasance/potctl" ]; then - echo 'flavors=["iofog","datasance"]' >> "$GITHUB_OUTPUT" - else - echo 'flavors=["iofog"]' >> "$GITHUB_OUTPUT" - fi - release: - needs: release-matrix - strategy: - matrix: - flavor: ${{ fromJson(needs.release-matrix.outputs.flavors) }} - name: Release (${{ matrix.flavor }}) runs-on: ubuntu-latest permissions: contents: write @@ -49,7 +31,7 @@ jobs: - name: Release env: - FLAVOR: ${{ matrix.flavor }} + 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/.goreleaser.yml b/.goreleaser.yml index 5df6907bc..5a1c5cfce 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,7 +1,7 @@ # Unified goreleaser config for potctl (datasance) and iofogctl (iofog). -# Set FLAVOR and load version pins before running: -# FLAVOR=iofog script/goreleaser-release.sh release --clean -# FLAVOR=datasance script/goreleaser-release.sh release --clean +# 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 }}' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9c729e9..ee20f22ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ First greenfield v3.8 release candidate. Dual-flavor build (`potctl` / `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` with `FLAVOR=datasance|iofog` matrix +- 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) diff --git a/CONTRIBUTING b/CONTRIBUTING index ad6b12011..0037d7fc6 100644 --- a/CONTRIBUTING +++ b/CONTRIBUTING @@ -63,21 +63,23 @@ Local checks: `make lint`, `make test-unit`, `make grep-gates`, `make security-c ## Release builds -GoReleaser uses a single root `.goreleaser.yml` driven by `FLAVOR`: +GoReleaser uses a single root `.goreleaser.yml`. Each mirror releases one binary: -```bash -FLAVOR=iofog script/goreleaser-release.sh release --clean -FLAVOR=datasance script/goreleaser-release.sh release --clean -``` +| GitHub repo | Binary | +|-------------|--------| +| `Datasance/potctl` | `potctl` | +| `eclipse-iofog/iofogctl` | `iofogctl` | -Version pins come from `versions.mk`; flavor-specific ldflags are exported by `script/goreleaser-env.sh` before goreleaser runs. Local snapshot (requires Linux cross-compilers for the full matrix): +`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 -FLAVOR=iofog script/goreleaser-release.sh release --snapshot --clean -FLAVOR=datasance script/goreleaser-release.sh release --snapshot --clean +GITHUB_REPOSITORY=Datasance/potctl script/goreleaser-release.sh release --snapshot --clean +GITHUB_REPOSITORY=eclipse-iofog/iofogctl script/goreleaser-release.sh release --snapshot --clean ``` -Validate config only: `FLAVOR=iofog script/goreleaser-release.sh check` +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/script/goreleaser-env-check.sh b/script/goreleaser-env-check.sh index 9ffe49ba8..1221920f2 100755 --- a/script/goreleaser-env-check.sh +++ b/script/goreleaser-env-check.sh @@ -2,6 +2,6 @@ set -euo pipefail if [ -z "${OPERATOR_VERSION:-}" ]; then - echo "Load release env first: eval \"\$(FLAVOR=${FLAVOR:-iofog} script/goreleaser-env.sh)\"" >&2 + 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 index c1f5f602d..72c70864e 100755 --- a/script/goreleaser-env.sh +++ b/script/goreleaser-env.sh @@ -1,14 +1,32 @@ #!/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 "$(FLAVOR=iofog script/goreleaser-env.sh)" -# FLAVOR=datasance script/goreleaser-release.sh release --clean +# 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" -FLAVOR="${FLAVOR:-iofog}" +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 diff --git a/script/goreleaser-release.sh b/script/goreleaser-release.sh index 3f0ee9f36..c1eb2bca2 100755 --- a/script/goreleaser-release.sh +++ b/script/goreleaser-release.sh @@ -4,6 +4,5 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" -FLAVOR="${FLAVOR:-iofog}" eval "$("$ROOT/script/goreleaser-env.sh")" exec goreleaser "$@"