diff --git a/.gitignore b/.gitignore index c0b18e64e..3fb3aecc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ output bundle pkg/goods/bins +pkg/goods/internal/bins pkg/goods/images *tgz .pre-commit-config.yaml diff --git a/Makefile b/Makefile index 9c1133159..649d0510b 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ ARCH := $(shell uname -m) APP_NAME = embedded-cluster ADMIN_CONSOLE_CHART_URL = oci://registry.replicated.com/library ADMIN_CONSOLE_CHART_NAME = admin-console -ADMIN_CONSOLE_CHART_VERSION = 1.108.1 +ADMIN_CONSOLE_CHART_VERSION = 1.108.3 ADMIN_CONSOLE_IMAGE_OVERRIDE = ADMIN_CONSOLE_MIGRATIONS_IMAGE_OVERRIDE = EMBEDDED_OPERATOR_CHART_URL = oci://registry.replicated.com/library @@ -21,6 +21,7 @@ KUBECTL_VERSION = v1.29.3 K0S_VERSION = v1.29.2+k0s.0 K0S_BINARY_SOURCE_OVERRIDE = TROUBLESHOOT_VERSION = v0.84.1 +KOTS_VERSION = v$(shell echo $(ADMIN_CONSOLE_CHART_VERSION) | sed 's/\([0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/') LD_FLAGS = -X github.com/replicatedhq/embedded-cluster/pkg/defaults.K0sVersion=$(K0S_VERSION) \ -X github.com/replicatedhq/embedded-cluster/pkg/defaults.Version=$(VERSION) \ -X github.com/replicatedhq/embedded-cluster/pkg/defaults.K0sBinaryURL=$(K0S_BINARY_SOURCE_OVERRIDE) \ @@ -81,6 +82,14 @@ pkg/goods/bins/local-artifact-mirror: Makefile mkdir -p pkg/goods/bins CGO_ENABLED=0 go build -o pkg/goods/bins/local-artifact-mirror ./cmd/local-artifact-mirror +pkg/goods/internal/bins/kubectl-kots: Makefile + mkdir -p pkg/goods/internal/bins + mkdir -p output/tmp/kots + curl -L -o output/tmp/kots/kots.tar.gz https://github.com/replicatedhq/kots/releases/download/$(KOTS_VERSION)/kots_linux_amd64.tar.gz + tar -xzf output/tmp/kots/kots.tar.gz -C output/tmp/kots + mv output/tmp/kots/kots pkg/goods/internal/bins/kubectl-kots + touch pkg/goods/internal/bins/kubectl-kots + output/tmp/release.tar.gz: e2e/kots-release-install/* mkdir -p output/tmp tar -czf output/tmp/release.tar.gz -C e2e/kots-release-install . @@ -102,7 +111,8 @@ static: pkg/goods/bins/k0s \ pkg/goods/bins/kubectl-preflight \ pkg/goods/bins/kubectl \ pkg/goods/bins/kubectl-support_bundle \ - pkg/goods/bins/local-artifact-mirror + pkg/goods/bins/local-artifact-mirror \ + pkg/goods/internal/bins/kubectl-kots .PHONY: embedded-cluster-linux-amd64 embedded-cluster-linux-amd64: static go.mod diff --git a/cmd/embedded-cluster/install.go b/cmd/embedded-cluster/install.go index 613249676..671a5ba26 100644 --- a/cmd/embedded-cluster/install.go +++ b/cmd/embedded-cluster/install.go @@ -1,10 +1,8 @@ package main import ( - "bytes" "fmt" "os" - "os/exec" "path/filepath" "time" @@ -19,7 +17,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/defaults" "github.com/replicatedhq/embedded-cluster/pkg/goods" "github.com/replicatedhq/embedded-cluster/pkg/helpers" - "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/preflights" "github.com/replicatedhq/embedded-cluster/pkg/prompts" @@ -33,26 +30,6 @@ import ( // necessary data to the screen). var ErrNothingElseToAdd = fmt.Errorf("") -// runCommand spawns a command and capture its output. Outputs are logged using the -// logrus package and stdout is returned as a string. -func runCommand(bin string, args ...string) (string, error) { - fullcmd := append([]string{bin}, args...) - logrus.Debugf("running command: %v", fullcmd) - - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) - cmd := exec.Command(bin, args...) - cmd.Stdout = stdout - cmd.Stderr = stderr - if err := cmd.Run(); err != nil { - logrus.Debugf("failed to run command:") - logrus.Debugf("stdout: %s", stdout.String()) - logrus.Debugf("stderr: %s", stderr.String()) - return "", err - } - return stdout.String(), nil -} - // installAndEnableLocalArtifactMirror installs and enables the local artifact mirror. This // service is responsible for serving on localhost, through http, all files that are used // during a cluster upgrade. @@ -65,13 +42,13 @@ func installAndEnableLocalArtifactMirror() error { if err := goods.MaterializeLocalArtifactMirrorUnitFile(); err != nil { return fmt.Errorf("failed to materialize artifact mirror unit: %w", err) } - if _, err := runCommand("systemctl", "daemon-reload"); err != nil { + if _, err := helpers.RunCommand("systemctl", "daemon-reload"); err != nil { return fmt.Errorf("unable to get reload systemctl daemon: %w", err) } - if _, err := runCommand("systemctl", "start", "local-artifact-mirror"); err != nil { + if _, err := helpers.RunCommand("systemctl", "start", "local-artifact-mirror"); err != nil { return fmt.Errorf("unable to start the local artifact mirror: %w", err) } - if _, err := runCommand("systemctl", "enable", "local-artifact-mirror"); err != nil { + if _, err := helpers.RunCommand("systemctl", "enable", "local-artifact-mirror"); err != nil { return fmt.Errorf("unable to start the local artifact mirror: %w", err) } return nil @@ -85,7 +62,7 @@ func runPostInstall() error { if err := os.Symlink(src, dst); err != nil { return fmt.Errorf("failed to create symlink: %w", err) } - if _, err := runCommand("systemctl", "daemon-reload"); err != nil { + if _, err := helpers.RunCommand("systemctl", "daemon-reload"); err != nil { return fmt.Errorf("unable to get reload systemctl daemon: %w", err) } return installAndEnableLocalArtifactMirror() @@ -266,15 +243,11 @@ func ensureK0sConfig(c *cli.Context) error { if c.Bool("no-prompt") { opts = append(opts, addons.WithoutPrompt()) } - if c.String("license") != "" { - license, err := helpers.ParseLicense(c.String("license")) - if err != nil { - return fmt.Errorf("unable to parse license: %w", err) - } - opts = append(opts, addons.WithLicense(license)) + if l := c.String("license"); l != "" { + opts = append(opts, addons.WithLicense(l)) } - if c.String("airgap-bundle") != "" { - opts = append(opts, addons.Airgap()) + if ab := c.String("airgap-bundle"); ab != "" { + opts = append(opts, addons.WithAirgapBundle(ab)) } if err := config.UpdateHelmConfigs(cfg, opts...); err != nil { return fmt.Errorf("unable to update helm configs: %w", err) @@ -337,10 +310,10 @@ func installK0s() error { if err := helpers.MoveFile(ourbin, hstbin); err != nil { return fmt.Errorf("unable to move k0s binary: %w", err) } - if _, err := runCommand(hstbin, config.InstallFlags()...); err != nil { + if _, err := helpers.RunCommand(hstbin, config.InstallFlags()...); err != nil { return fmt.Errorf("unable to install: %w", err) } - if _, err := runCommand(hstbin, "start"); err != nil { + if _, err := helpers.RunCommand(hstbin, "start"); err != nil { return fmt.Errorf("unable to start: %w", err) } return nil @@ -365,49 +338,19 @@ func waitForK0s() error { if !success { return fmt.Errorf("timeout waiting for %s", defaults.BinaryName()) } - if _, err := runCommand(defaults.K0sBinaryPath(), "status"); err != nil { + if _, err := helpers.RunCommand(defaults.K0sBinaryPath(), "status"); err != nil { return fmt.Errorf("unable to get status: %w", err) } loading.Infof("Node installation finished") return nil } -// createAirgapConfigMaps creates the airgap configmaps in the k8s cluster from the airgap file. -func createAirgapConfigMaps(c *cli.Context) error { - loading := spinner.Start() - defer loading.Close() - loading.Infof("Creating airgap configmaps") - // create k8s client - os.Setenv("KUBECONFIG", defaults.PathToKubeConfig()) - cli, err := kubeutils.KubeClient() - if err != nil { - return fmt.Errorf("failed to create k8s client: %w", err) - } - - // read file from path - rawfile, err := os.Open(c.String("airgap-bundle")) - if err != nil { - return fmt.Errorf("failed to open airgap file: %w", err) - } - defer rawfile.Close() - - if err = airgap.CreateAppConfigMaps(c.Context, cli, rawfile); err != nil { - return fmt.Errorf("unable to create airgap configmaps: %w", err) - } - loading.Infof("Airgap configmaps created") - return nil -} - // runOutro calls Outro() in all enabled addons by means of Applier. func runOutro(c *cli.Context) error { os.Setenv("KUBECONFIG", defaults.PathToKubeConfig()) opts := []addons.Option{} - if c.String("license") != "" { - license, err := helpers.ParseLicense(c.String("license")) - if err != nil { - return fmt.Errorf("unable to parse license: %w", err) - } - opts = append(opts, addons.WithLicense(license)) + if l := c.String("license"); l != "" { + opts = append(opts, addons.WithLicense(l)) } if c.String("overrides") != "" { eucfg, err := helpers.ParseEndUserConfig(c.String("overrides")) @@ -416,8 +359,8 @@ func runOutro(c *cli.Context) error { } opts = append(opts, addons.WithEndUserConfig(eucfg)) } - if c.String("airgap-bundle") != "" { - opts = append(opts, addons.Airgap()) + if ab := c.String("airgap-bundle"); ab != "" { + opts = append(opts, addons.WithAirgapBundle(ab)) } return addons.NewApplier(opts...).Outro(c.Context) } @@ -432,7 +375,6 @@ var installCommand = &cli.Command{ if os.Getuid() != 0 { return fmt.Errorf("install command must be run as root") } - if c.String("airgap-bundle") != "" { metrics.DisableMetrics() } @@ -520,13 +462,6 @@ var installCommand = &cli.Command{ metrics.ReportApplyFinished(c, err) return err } - if c.String("airgap-bundle") != "" { - err := createAirgapConfigMaps(c) - if err != nil { - err = fmt.Errorf("unable to create airgap configmaps: %w", err) - return err - } - } logrus.Debugf("running outro") if err := runOutro(c); err != nil { metrics.ReportApplyFinished(c, err) diff --git a/cmd/embedded-cluster/join.go b/cmd/embedded-cluster/join.go index 998587e6e..fca318859 100644 --- a/cmd/embedded-cluster/join.go +++ b/cmd/embedded-cluster/join.go @@ -317,7 +317,7 @@ func installK0sBinary() error { // startK0sService starts the k0s service. func startK0sService() error { - if _, err := runCommand(defaults.K0sBinaryPath(), "start"); err != nil { + if _, err := helpers.RunCommand(defaults.K0sBinaryPath(), "start"); err != nil { return fmt.Errorf("unable to start: %w", err) } return nil @@ -339,7 +339,7 @@ func createSystemdUnitFiles(fullcmd string) error { if err := os.Symlink(src, dst); err != nil { return err } - if _, err := runCommand("systemctl", "daemon-reload"); err != nil { + if _, err := helpers.RunCommand("systemctl", "daemon-reload"); err != nil { return err } return installAndEnableLocalArtifactMirror() @@ -353,7 +353,7 @@ func runK0sInstallCommand(fullcmd string) error { if strings.Contains(fullcmd, "controller") { args = append(args, "--disable-components", "konnectivity-server", "--enable-dynamic-config") } - if _, err := runCommand(args[0], args[1:]...); err != nil { + if _, err := helpers.RunCommand(args[0], args[1:]...); err != nil { return err } return nil diff --git a/cmd/embedded-cluster/uninstall.go b/cmd/embedded-cluster/uninstall.go index 341a9d95e..7535d6730 100644 --- a/cmd/embedded-cluster/uninstall.go +++ b/cmd/embedded-cluster/uninstall.go @@ -18,6 +18,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/replicatedhq/embedded-cluster/pkg/defaults" + "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/prompts" ) @@ -395,7 +396,7 @@ var resetCommand = &cli.Command{ lamPath := "/etc/systemd/system/local-artifact-mirror.service" if _, err := os.Stat(lamPath); err == nil { - if _, err := runCommand("systemctl", "stop", "local-artifact-mirror"); err != nil { + if _, err := helpers.RunCommand("systemctl", "stop", "local-artifact-mirror"); err != nil { return err } if err := os.RemoveAll(lamPath); err != nil { diff --git a/e2e/scripts/default-install.sh b/e2e/scripts/default-install.sh index c42c6fbb0..2b8009f3b 100644 --- a/e2e/scripts/default-install.sh +++ b/e2e/scripts/default-install.sh @@ -67,15 +67,15 @@ main() { exit 1 fi if ! grep -q "Admin Console is ready!" /tmp/log; then - echo "Failed to install embedded-cluster" + echo "Failed to validate that the Admin Console is ready" exit 1 fi if ! wait_for_healthy_node; then - echo "Failed to install embedded-cluster" + echo "Failed to wait for healthy node" exit 1 fi if ! wait_for_pods_running 900; then - echo "Failed to install embedded-cluster" + echo "Failed to wait for pods to be running" exit 1 fi if ! check_openebs_storage_class; then diff --git a/e2e/scripts/embedded-preflight.sh b/e2e/scripts/embedded-preflight.sh index 38e269c7e..bb8b8be99 100644 --- a/e2e/scripts/embedded-preflight.sh +++ b/e2e/scripts/embedded-preflight.sh @@ -169,7 +169,7 @@ main() { exit 1 fi if ! grep -q "Admin Console is ready!" /tmp/log; then - echo "Failed to install embedded-cluster" + echo "Failed to validate that the Admin Console is ready" exit 1 fi if ! has_applied_host_preflight; then @@ -178,7 +178,7 @@ main() { exit 1 fi if ! wait_for_healthy_node; then - echo "Failed to install embedded-cluster" + echo "Failed to wait for healthy node" exit 1 fi if ! systemctl restart embedded-cluster; then diff --git a/e2e/scripts/pre-minio-removal-install.sh b/e2e/scripts/pre-minio-removal-install.sh index 64846cff5..e831a8019 100644 --- a/e2e/scripts/pre-minio-removal-install.sh +++ b/e2e/scripts/pre-minio-removal-install.sh @@ -216,7 +216,7 @@ main() { exit 1 fi if ! grep -q "Admin Console is ready!" /tmp/log; then - echo "Failed to install embedded-cluster" + echo "Failed to validate that the Admin Console is ready" exit 1 fi if ! install_kots_cli; then @@ -224,7 +224,7 @@ main() { exit 1 fi if ! wait_for_healthy_node; then - echo "Failed to install embedded-cluster" + echo "Failed to wait for healthy node" exit 1 fi if ! ensure_node_config; then @@ -238,7 +238,7 @@ main() { fi fi if ! wait_for_pods_running 900; then - echo "Failed to install embedded-cluster" + echo "Failed to wait for pods to be running" exit 1 fi if ! check_openebs_storage_class; then diff --git a/e2e/scripts/single-node-install.sh b/e2e/scripts/single-node-install.sh index 64846cff5..7e1861509 100644 --- a/e2e/scripts/single-node-install.sh +++ b/e2e/scripts/single-node-install.sh @@ -144,6 +144,10 @@ deploy_app() { echo "kotsadm logs" kubectl logs -n kotsadm -l app=kotsadm --tail=50 + kubectl logs -n kotsadm -l app=kotsadm --tail=50 --previous + + echo "all pods" + kubectl get pods -A } wait_for_nginx_pods() { @@ -216,7 +220,7 @@ main() { exit 1 fi if ! grep -q "Admin Console is ready!" /tmp/log; then - echo "Failed to install embedded-cluster" + echo "Failed to validate that the Admin Console is ready" exit 1 fi if ! install_kots_cli; then @@ -224,7 +228,7 @@ main() { exit 1 fi if ! wait_for_healthy_node; then - echo "Failed to install embedded-cluster" + echo "Failed to wait for healthy node" exit 1 fi if ! ensure_node_config; then @@ -238,7 +242,7 @@ main() { fi fi if ! wait_for_pods_running 900; then - echo "Failed to install embedded-cluster" + echo "Failed to wait for pods to be running" exit 1 fi if ! check_openebs_storage_class; then diff --git a/e2e/scripts/unsupported-overrides.sh b/e2e/scripts/unsupported-overrides.sh index 751809a34..07e518819 100644 --- a/e2e/scripts/unsupported-overrides.sh +++ b/e2e/scripts/unsupported-overrides.sh @@ -46,7 +46,6 @@ spec: version: 1.108.0-build.1 values: | isHelmManaged: false - kotsApplication: default value minimalRBAC: false service: nodePort: 30000 @@ -133,7 +132,7 @@ main() { exit 1 fi if ! grep -q "Admin Console is ready!" /tmp/log; then - echo "Failed to install embedded-cluster" + echo "Failed to validate that the Admin Console is ready" exit 1 fi if ! override_applied; then diff --git a/pkg/addons/adminconsole/adminconsole.go b/pkg/addons/adminconsole/adminconsole.go index 153427db0..22eeadbbb 100644 --- a/pkg/addons/adminconsole/adminconsole.go +++ b/pkg/addons/adminconsole/adminconsole.go @@ -6,11 +6,11 @@ import ( "context" "encoding/base64" "fmt" + "os" "time" "github.com/k0sproject/dig" "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" @@ -18,10 +18,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" - k8syaml "sigs.k8s.io/yaml" "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" "github.com/replicatedhq/embedded-cluster/pkg/defaults" + "github.com/replicatedhq/embedded-cluster/pkg/goods" + "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/prompts" @@ -43,7 +44,7 @@ var ( ) // protectedFields are helm values that are not overwritten when upgrading the addon. -var protectedFields = []string{"automation", "embeddedClusterID", "kotsApplication"} +var protectedFields = []string{"automation", "embeddedClusterID"} const DEFAULT_ADMIN_CONSOLE_NODE_PORT = 30000 @@ -76,11 +77,11 @@ func init() { // AdminConsole manages the admin console helm chart installation. type AdminConsole struct { - namespace string - useprompt bool - airgap bool - config v1beta1.ClusterConfig - license *kotsv1beta1.License + namespace string + useprompt bool + config v1beta1.ClusterConfig + licenseFile string + airgapBundle string } func (a *AdminConsole) askPassword() (string, error) { @@ -122,39 +123,6 @@ func (a *AdminConsole) HostPreflights() (*v1beta2.HostPreflightSpec, error) { return release.GetHostPreflights() } -// addLicenseAndVersionToHelmValues adds the embedded license to the helm values. -func (a *AdminConsole) addLicenseAndVersionToHelmValues() error { - if a.license == nil { - return nil - } - raw, err := k8syaml.Marshal(a.license) - if err != nil { - return fmt.Errorf("unable to marshal license: %w", err) - } - var appVersion string - if channelRelease, err := release.GetChannelRelease(); err != nil { - return fmt.Errorf("unable to get channel release: %w", err) - } else if channelRelease != nil { - appVersion = channelRelease.VersionLabel - } - - isAirgap := "false" - if a.airgap { - isAirgap = "true" - } - - helmValues["automation"] = map[string]interface{}{ - "appVersionLabel": appVersion, - "license": map[string]interface{}{ - "slug": a.license.Spec.AppSlug, - "data": string(raw), - }, - "airgap": isAirgap, - } - - return nil -} - // getPasswordFromConfig returns the adminconsole password from the provided chart config. func getPasswordFromConfig(chart *v1beta1.Chart) (string, error) { if chart.Values == "" { @@ -206,20 +174,6 @@ func (a *AdminConsole) addPasswordToHelmValues() error { return nil } -// addKotsApplicationToHelmValues extracts the embed application struct found in this binary -// and adds it to the helm values. -func (a *AdminConsole) addKotsApplicationToHelmValues() error { - app, err := release.GetApplication() - if err != nil { - return fmt.Errorf("unable to get application: %w", err) - } else if app == nil { - helmValues["kotsApplication"] = "default value" - return nil - } - helmValues["kotsApplication"] = string(app) - return nil -} - // GenerateHelmConfig generates the helm config for the adminconsole and writes the charts to // the disk. func (a *AdminConsole) GenerateHelmConfig(onlyDefaults bool) ([]v1beta1.Chart, []v1beta1.Repository, error) { @@ -227,14 +181,8 @@ func (a *AdminConsole) GenerateHelmConfig(onlyDefaults bool) ([]v1beta1.Chart, [ if err := a.addPasswordToHelmValues(); err != nil { return nil, nil, fmt.Errorf("unable to add password to helm values: %w", err) } - if err := a.addKotsApplicationToHelmValues(); err != nil { - return nil, nil, fmt.Errorf("unable to add kots app to helm values: %w", err) - } helmValues["embeddedClusterID"] = metrics.ClusterID().String() } - if err := a.addLicenseAndVersionToHelmValues(); err != nil { - return nil, nil, fmt.Errorf("unable to add license to helm values: %w", err) - } values, err := yaml.Marshal(helmValues) if err != nil { return nil, nil, fmt.Errorf("unable to marshal helm values: %w", err) @@ -256,7 +204,7 @@ func (a *AdminConsole) Outro(ctx context.Context, cli client.Client) error { backoff := wait.Backoff{Steps: 60, Duration: 5 * time.Second, Factor: 1.0, Jitter: 0.1} loading.Infof("Waiting for Admin Console to deploy: 0/2 ready") - if a.airgap { + if a.airgapBundle != "" { err := createRegistrySecret(ctx, cli, a.namespace) if err != nil { loading.Close() @@ -286,13 +234,60 @@ func (a *AdminConsole) Outro(ctx context.Context, cli client.Client) error { loading.Close() return fmt.Errorf("error waiting for admin console: %v", lasterr) } + + if a.licenseFile == "" { + loading.Closef("Admin Console is ready!") + return nil + } + + loading.Infof("Finalizing") + + kotsBinPath, err := goods.MaterializeInternalBinary("kubectl-kots") + if err != nil { + loading.Close() + return fmt.Errorf("unable to materialize kubectl-kots binary: %w", err) + } + defer os.Remove(kotsBinPath) + + license, err := helpers.ParseLicense(a.licenseFile) + if err != nil { + return fmt.Errorf("unable to parse license: %w", err) + } + + var appVersionLabel string + if channelRelease, err := release.GetChannelRelease(); err != nil { + return fmt.Errorf("unable to get channel release: %w", err) + } else if channelRelease != nil { + appVersionLabel = channelRelease.VersionLabel + } + + installArgs := []string{ + "install", + license.Spec.AppSlug, + "--license-file", + a.licenseFile, + "--namespace", + a.namespace, + "--app-version-label", + appVersionLabel, + "--exclude-admin-console", + } + if a.airgapBundle != "" { + installArgs = append(installArgs, "--airgap-bundle", a.airgapBundle) + } + + if _, err := helpers.RunCommand(kotsBinPath, installArgs...); err != nil { + loading.Close() + return fmt.Errorf("unable to install the application: %w", err) + } + loading.Closef("Admin Console is ready!") - a.printSuccessMessage() + a.printSuccessMessage(license.Spec.AppSlug) return nil } // printSuccessMessage prints the success message when the admin console is online. -func (a *AdminConsole) printSuccessMessage() { +func (a *AdminConsole) printSuccessMessage(appSlug string) { successColor := "\033[32m" colorReset := "\033[0m" ipaddr := defaults.TryDiscoverPublicIP() @@ -304,20 +299,20 @@ func (a *AdminConsole) printSuccessMessage() { ipaddr = "NODE-IP-ADDRESS" } } - successMessage := fmt.Sprintf("Admin Console accessible at: %shttp://%s:%v%s", - successColor, ipaddr, DEFAULT_ADMIN_CONSOLE_NODE_PORT, colorReset, + successMessage := fmt.Sprintf("Visit the admin console to configure and install %s: %shttp://%s:%v%s", + appSlug, successColor, ipaddr, DEFAULT_ADMIN_CONSOLE_NODE_PORT, colorReset, ) logrus.Info(successMessage) } // New creates a new AdminConsole object. -func New(ns string, useprompt bool, config v1beta1.ClusterConfig, license *kotsv1beta1.License, airgap bool) (*AdminConsole, error) { +func New(ns string, useprompt bool, config v1beta1.ClusterConfig, licenseFile string, airgapBundle string) (*AdminConsole, error) { return &AdminConsole{ - namespace: ns, - useprompt: useprompt, - config: config, - license: license, - airgap: airgap, + namespace: ns, + useprompt: useprompt, + config: config, + licenseFile: licenseFile, + airgapBundle: airgapBundle, }, nil } diff --git a/pkg/addons/applier.go b/pkg/addons/applier.go index 1228d4c2a..3f5c30484 100644 --- a/pkg/addons/applier.go +++ b/pkg/addons/applier.go @@ -10,7 +10,6 @@ import ( "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -39,10 +38,10 @@ type Applier struct { prompt bool verbose bool config v1beta1.ClusterConfig - license *kotsv1beta1.License + licenseFile string onlyDefaults bool endUserConfig *embeddedclusterv1beta1.Config - airgap bool + airgapBundle string } // Outro runs the outro in all enabled add-ons. @@ -147,19 +146,19 @@ func (a *Applier) load() ([]AddOn, error) { } addons = append(addons, obs) - reg, err := registry.New(a.airgap) + reg, err := registry.New(a.airgapBundle != "") if err != nil { return nil, fmt.Errorf("unable to create registry addon: %w", err) } addons = append(addons, reg) - embedoperator, err := embeddedclusteroperator.New(a.endUserConfig, a.license) + embedoperator, err := embeddedclusteroperator.New(a.endUserConfig, a.licenseFile) if err != nil { return nil, fmt.Errorf("unable to create embedded cluster operator addon: %w", err) } addons = append(addons, embedoperator) - aconsole, err := adminconsole.New(defaults.KotsadmNamespace, a.prompt, a.config, a.license, a.airgap) + aconsole, err := adminconsole.New(defaults.KotsadmNamespace, a.prompt, a.config, a.licenseFile, a.airgapBundle) if err != nil { return nil, fmt.Errorf("unable to create admin console addon: %w", err) } @@ -228,10 +227,11 @@ func (a *Applier) waitForKubernetes(ctx context.Context) error { // NewApplier creates a new Applier instance with all addons registered. func NewApplier(opts ...Option) *Applier { applier := &Applier{ - prompt: true, - verbose: true, - config: v1beta1.ClusterConfig{}, - license: nil, + prompt: true, + verbose: true, + config: v1beta1.ClusterConfig{}, + licenseFile: "", + airgapBundle: "", } for _, fn := range opts { fn(applier) diff --git a/pkg/addons/embeddedclusteroperator/embeddedclusteroperator.go b/pkg/addons/embeddedclusteroperator/embeddedclusteroperator.go index 24ce07d72..efe110e7e 100644 --- a/pkg/addons/embeddedclusteroperator/embeddedclusteroperator.go +++ b/pkg/addons/embeddedclusteroperator/embeddedclusteroperator.go @@ -17,6 +17,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole" "github.com/replicatedhq/embedded-cluster/pkg/defaults" + "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/release" @@ -46,7 +47,7 @@ type EmbeddedClusterOperator struct { namespace string deployName string endUserConfig *embeddedclusterv1beta1.Config - license *kotsv1beta1.License + licenseFile string } // Version returns the version of the embedded cluster operator chart. @@ -115,13 +116,21 @@ func (e *EmbeddedClusterOperator) Outro(ctx context.Context, cli client.Client) if e.endUserConfig != nil { euOverrides = e.endUserConfig.Spec.UnsupportedOverrides.K0s } + var license *kotsv1beta1.License + if e.licenseFile != "" { + l, err := helpers.ParseLicense(e.licenseFile) + if err != nil { + return fmt.Errorf("unable to parse license: %w", err) + } + license = l + } installation := embeddedclusterv1beta1.Installation{ ObjectMeta: metav1.ObjectMeta{ Name: time.Now().Format("20060102150405"), }, Spec: embeddedclusterv1beta1.InstallationSpec{ ClusterID: metrics.ClusterID().String(), - MetricsBaseURL: metrics.BaseURL(e.license), + MetricsBaseURL: metrics.BaseURL(license), AirGap: false, Config: cfgspec, EndUserK0sConfigOverrides: euOverrides, @@ -135,11 +144,11 @@ func (e *EmbeddedClusterOperator) Outro(ctx context.Context, cli client.Client) } // New creates a new EmbeddedClusterOperator addon. -func New(endUserConfig *embeddedclusterv1beta1.Config, license *kotsv1beta1.License) (*EmbeddedClusterOperator, error) { +func New(endUserConfig *embeddedclusterv1beta1.Config, licenseFile string) (*EmbeddedClusterOperator, error) { return &EmbeddedClusterOperator{ namespace: "embedded-cluster", deployName: "embedded-cluster-operator", endUserConfig: endUserConfig, - license: license, + licenseFile: licenseFile, }, nil } diff --git a/pkg/addons/options.go b/pkg/addons/options.go index c1dfe61ae..57b2336ba 100644 --- a/pkg/addons/options.go +++ b/pkg/addons/options.go @@ -3,7 +3,6 @@ package addons import ( "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" ) // Option sets and option on an Applier reference. @@ -47,15 +46,15 @@ func WithEndUserConfig(config *embeddedclusterv1beta1.Config) Option { } // WithLicense sets the license for the application. -func WithLicense(license *kotsv1beta1.License) Option { +func WithLicense(licenseFile string) Option { return func(a *Applier) { - a.license = license + a.licenseFile = licenseFile } } -// Airgap sets the application to be installed in airgap mode -func Airgap() Option { +// WithAirgapBundle sets the airgap bundle for the application to be installed in airgap mode. +func WithAirgapBundle(airgapBundle string) Option { return func(a *Applier) { - a.airgap = true + a.airgapBundle = airgapBundle } } diff --git a/pkg/airgap/app_configmaps.go b/pkg/airgap/app_configmaps.go deleted file mode 100644 index 28efba53a..000000000 --- a/pkg/airgap/app_configmaps.go +++ /dev/null @@ -1,173 +0,0 @@ -package airgap - -import ( - "archive/tar" - "compress/gzip" - "context" - "encoding/base64" - "fmt" - "io" - - "github.com/gosimple/slug" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/replicatedhq/embedded-cluster/pkg/defaults" - "github.com/replicatedhq/embedded-cluster/pkg/release" -) - -func CreateAppConfigMaps(ctx context.Context, cli client.Client, airgapReader io.Reader) error { - err := createNamespaceIfNotExist(ctx, cli, defaults.KotsadmNamespace) - if err != nil { - return fmt.Errorf("failed to create namespace: %w", err) - } - - // decompress tarball - ungzip, err := gzip.NewReader(airgapReader) - if err != nil { - return fmt.Errorf("failed to decompress airgap file: %w", err) - } - - // iterate through tarball - tarreader := tar.NewReader(ungzip) - var nextFile *tar.Header - foundAppRelease, foundAirgapYaml := false, false - for { - nextFile, err = tarreader.Next() - if err != nil { - if err == io.EOF { - if !foundAppRelease { - return fmt.Errorf("app release not found in airgap file") - } - if !foundAirgapYaml { - return fmt.Errorf("airgap.yaml not found in airgap file") - } - } - return fmt.Errorf("failed to read airgap file: %w", err) - } - - if nextFile.Name == "airgap.yaml" { - foundAirgapYaml = true - var contents []byte - contents, err = io.ReadAll(tarreader) - if err != nil { - return fmt.Errorf("failed to read airgap.yaml file within airgap file: %w", err) - } - err = createAppConfigMap(ctx, cli, "meta", "airgap.yaml", contents) - if err != nil { - return fmt.Errorf("failed to create app configmap: %w", err) - } - } - if nextFile.Name == "app.tar.gz" { - foundAppRelease = true - err = createAppYamlConfigMaps(ctx, cli, tarreader) - if err != nil { - return fmt.Errorf("failed to read app release file within airgap file: %w", err) - } - } - if foundAppRelease && foundAirgapYaml { - break - } - } - - return nil -} - -func createAppYamlConfigMaps(ctx context.Context, cli client.Client, apptarball io.Reader) error { - // read file from airgapFile - // decompress tarball - // iterate through tarball - // create configmap each file in tarball - - ungzip, err := gzip.NewReader(apptarball) - if err != nil { - return fmt.Errorf("failed to decompress app release file: %w", err) - } - - tarreader := tar.NewReader(ungzip) - var nextFile *tar.Header - for { - nextFile, err = tarreader.Next() - if err != nil { - if err == io.EOF { - break - } - return fmt.Errorf("failed to read app release file: %w", err) - } - - var contents []byte - contents, err = io.ReadAll(tarreader) - - if err != nil { - return fmt.Errorf("failed to read app release file %s: %w", nextFile.Name, err) - } - - err = createAppConfigMap(ctx, cli, nextFile.Name, nextFile.Name, contents) - if err != nil { - return fmt.Errorf("failed to create app configmap: %w", err) - } - } - - return nil -} - -func createNamespaceIfNotExist(ctx context.Context, cli client.Client, name string) error { - existingNs := &corev1.Namespace{} - - err := cli.Get(ctx, client.ObjectKey{Name: name}, existingNs) - if err == nil { - return nil - } - - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - } - - err = cli.Create(ctx, ns) - if err != nil { - return fmt.Errorf("failed to create namespace %s: %w", ns.Name, err) - } - - return nil -} - -func createAppConfigMap(ctx context.Context, cli client.Client, name string, filename string, contents []byte) error { - rel, err := release.GetChannelRelease() - if err != nil { - return fmt.Errorf("failed to get channel release: %w", err) - } - if rel == nil { - rel = &release.ChannelRelease{ - AppSlug: "unknown", - } - } - - configMap := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("kotsadm-airgap-%s", slug.Make(name)), - Namespace: defaults.KotsadmNamespace, - Labels: map[string]string{ - "kots.io/automation": "airgap", - "kots.io/app": rel.AppSlug, - "kots.io/kotsadm": "true", - }, - }, - Data: map[string]string{ - filename: base64.StdEncoding.EncodeToString(contents), - }, - } - - err = cli.Create(ctx, configMap) - if err != nil { - return fmt.Errorf("failed to create configmap %s: %w", configMap.Name, err) - } - - return nil -} diff --git a/pkg/airgap/app_configmaps_test.go b/pkg/airgap/app_configmaps_test.go deleted file mode 100644 index 692667b20..000000000 --- a/pkg/airgap/app_configmaps_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package airgap - -import ( - "archive/tar" - "compress/gzip" - "context" - "io" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - "github.com/replicatedhq/embedded-cluster/pkg/release" -) - -func TestCreateAppConfigMaps(t *testing.T) { - releaseData := `# channel release object -channelID: "testID" -appSlug: "test-app-slug" -versionLabel: "test-version-label"` - err := release.SetReleaseDataForTests(map[string][]byte{ - "release.yaml": []byte(releaseData), - }) - if err != nil { - t.Fatal(err) - } - - tests := []struct { - name string - airgapDir string - airgapAppDir string - wantConfigmaps []corev1.ConfigMap - }{ - { - name: "tiny-airgap-noimages", - airgapDir: "tiny-airgap-noimages", - airgapAppDir: "tiny-airgap-noimages-app", - wantConfigmaps: []corev1.ConfigMap{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-airgap-meta", - Namespace: "kotsadm", - Labels: map[string]string{ - "kots.io/app": "test-app-slug", - "kots.io/automation": "airgap", - "kots.io/kotsadm": "true", - }, - }, - Data: map[string]string{ - "airgap.yaml": "YXBpVmVyc2lvbjoga290cy5pby92MWJldGExCmtpbmQ6IEFpcmdhcAptZXRhZGF0YToKICBjcmVhdGlvblRpbWVzdGFtcDogbnVsbApzcGVjOgogIGFwcFNsdWc6IGxhdmVyeWEtdGlueS1haXJnYXAKICBjaGFubmVsSUQ6IDJkTXJBcUpqclB6ZmVOSHY5YmMwZ0NIaDI1TgogIGNoYW5uZWxOYW1lOiBTdGFibGUKICBmb3JtYXQ6IGRvY2tlci1hcmNoaXZlCiAgcmVwbGljYXRlZENoYXJ0TmFtZXM6CiAgLSByZXBsaWNhdGVkCiAgLSByZXBsaWNhdGVkLXNkawogIHNhdmVkSW1hZ2VzOgogIC0gYWxwaW5lOjMuMTkuMQogIHNpZ25hdHVyZTogUFE0WnM0ZTRnMXNncmQxbFlvZzJpMjMraXhiRFhYM05hbmNPY0RkSytKcUQxUzRlbG1rSGhzR0lVYXpJbDE1ckw0WXVKUWR6ZWVtMGdlSzE0UEtBRE4rMFlMenZFVm05R3cxQ29xK3kzWkRwVW4yK09uN2NhSzRrMXZja0FFYm9tVUR3N0NtNUFHeFlERlBpejRpQytPbkttRllkZlU4RnFTTlQwaU1VeGpUdkJMZGxJZjlWT2g1d3NiaTVKNTExVUNFdjJIdDlVZXhjTkdvYmdvbHJDNUFVV0tBdmJING1HeG5TWFZSU0hqWHRzQUphSXcvQXcwUmRRODMwQUlhVEV6K0wrcWJnd2FzUUc3bEV4a2FRejJkRWJ5d1BQMm5MOVppeUNPSUFzamFLaWNsR3g4SHpLRENrN1dvbXQ0K1dPZnVzcXlrNm1VRmUvRWZsWC9sMlRBPT0KICB1cGRhdGVDdXJzb3I6ICIxIgogIHZlcnNpb25MYWJlbDogMC4xLjAKc3RhdHVzOiB7fQo=", - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-airgap-cluster-config-yaml", - Namespace: "kotsadm", - Labels: map[string]string{ - "kots.io/app": "test-app-slug", - "kots.io/automation": "airgap", - "kots.io/kotsadm": "true", - }, - }, - Data: map[string]string{ - "cluster-config.yaml": "YXBpVmVyc2lvbjogZW1iZWRkZWRjbHVzdGVyLnJlcGxpY2F0ZWQuY29tL3YxYmV0YTEKa2luZDogQ29uZmlnCm1ldGFkYXRhOgogIG5hbWU6ICJ0ZXN0LWNsdXN0ZXItY29uZmlnIgogIGFubm90YXRpb25zOgogICAga290cy5pby9leGNsdWRlOiAidHJ1ZSIKc3BlYzoKICB2ZXJzaW9uOiAiMS4yOS4yK2VjLjAiCiAgcm9sZXM6CiAgICBjb250cm9sbGVyOgogICAgICBsYWJlbHM6CiAgICAgICAgY29udHJvbGxlci10ZXN0LWxhYmVsOiBjb250cm9sbGVyLXRlc3QtbGFiZWwtdmFsdWUKICAgICAgbmFtZTogY29udHJvbGxlci1ub2RlCiAgICBjdXN0b206CiAgICAgIC0gbGFiZWxzOgogICAgICAgICAgYWJjLXRlc3QtbGFiZWw6IGFiYy10ZXN0LWxhYmVsLXZhbHVlCiAgICAgICAgICBhYmMtdGVzdC1sYWJlbC10d286IGFiYy10ZXN0LWxhYmVsLXZhbHVlLTIKICAgICAgICBuYW1lOiB3ZWIKICAgICAgLSBsYWJlbHM6CiAgICAgICAgICB4eXotdGVzdC1sYWJlbDogeHl6LXZhbHVlCiAgICAgICAgbmFtZTogYmFja2VuZAogICAgICAtIGxhYmVsczoKICAgICAgICAgIGVsYXN0aWNzZWFyY2gtbm9kZS1yb2xlOiBtYWluCiAgICAgICAgbmFtZTogZWxhc3RpY3NlYXJjaC1tYWluCiAgdW5zdXBwb3J0ZWRPdmVycmlkZXM6CiAgICBrMHM6IHwKICAgICAgY29uZmlnOgogICAgICAgIHNwZWM6CiAgICAgICAgICBhcGk6CiAgICAgICAgICAgIGV4dHJhQXJnczoKICAgICAgICAgICAgICBzZXJ2aWNlLW5vZGUtcG9ydC1yYW5nZTogODAtMzI3NjcKICBleHRlbnNpb25zOgogICAgaGVsbToKICAgICAgcmVwb3NpdG9yaWVzOgogICAgICAgIC0gbmFtZTogaW5ncmVzcy1uZ2lueAogICAgICAgICAgdXJsOiBodHRwczovL2t1YmVybmV0ZXMuZ2l0aHViLmlvL2luZ3Jlc3MtbmdpbngKICAgICAgY2hhcnRzOgogICAgICAgIC0gbmFtZTogaW5ncmVzcy1uZ2lueAogICAgICAgICAgY2hhcnRuYW1lOiBpbmdyZXNzLW5naW54L2luZ3Jlc3MtbmdpbngKICAgICAgICAgIG5hbWVzcGFjZTogaW5ncmVzcy1uZ2lueAogICAgICAgICAgdmVyc2lvbjogIjQuOS4xIgogICAgICAgICAgdmFsdWVzOiB8CiAgICAgICAgICAgIGNvbnRyb2xsZXI6CiAgICAgICAgICAgICAgc2VydmljZToKICAgICAgICAgICAgICAgIHR5cGU6IE5vZGVQb3J0CiAgICAgICAgICAgICAgICBub2RlUG9ydHM6CiAgICAgICAgICAgICAgICAgIGh0dHA6ICI4MCIKICAgICAgICAgICAgICAgICAgaHR0cHM6ICI0NDMiCiAgICAgICAgICAgICAgICBhbm5vdGF0aW9uczoKICAgICAgICAgICAgICAgICAgdGVzdC1jaGFydC1hbm5vdGF0aW9uOiB0ZXN0LWNoYXJ0LXZhbHVlCg==", - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-airgap-example-deployment-yaml", - Namespace: "kotsadm", - Labels: map[string]string{ - "kots.io/app": "test-app-slug", - "kots.io/automation": "airgap", - "kots.io/kotsadm": "true", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-airgap-k8s-app-yaml", - Namespace: "kotsadm", - Labels: map[string]string{ - "kots.io/app": "test-app-slug", - "kots.io/automation": "airgap", - "kots.io/kotsadm": "true", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-airgap-kots-app-yaml", - Namespace: "kotsadm", - Labels: map[string]string{ - "kots.io/app": "test-app-slug", - "kots.io/automation": "airgap", - "kots.io/kotsadm": "true", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-airgap-kots-config-yaml", - Namespace: "kotsadm", - Labels: map[string]string{ - "kots.io/app": "test-app-slug", - "kots.io/automation": "airgap", - "kots.io/kotsadm": "true", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-airgap-kots-lint-config-yaml", - Namespace: "kotsadm", - Labels: map[string]string{ - "kots.io/app": "test-app-slug", - "kots.io/automation": "airgap", - "kots.io/kotsadm": "true", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-airgap-kots-preflight-yaml", - Namespace: "kotsadm", - Labels: map[string]string{ - "kots.io/app": "test-app-slug", - "kots.io/automation": "airgap", - "kots.io/kotsadm": "true", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-airgap-kots-support-bundle-yaml", - Namespace: "kotsadm", - Labels: map[string]string{ - "kots.io/app": "test-app-slug", - "kots.io/automation": "airgap", - "kots.io/kotsadm": "true", - }, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := require.New(t) - ctx := context.Background() - - dir := "" - dir, err = os.Getwd() - req.NoError(err) - t.Logf("Current working directory: %s", dir) - - // create tarball stream from airgapAppDir - appTarballReader := createTarballFromDir(filepath.Join(dir, "testfiles", tt.airgapAppDir), nil) - var appTarballBytes []byte - appTarballBytes, err = io.ReadAll(appTarballReader) - req.NoError(err) - airgapReader := createTarballFromDir(filepath.Join(dir, "testfiles", tt.airgapDir), map[string][]byte{"app.tar.gz": appTarballBytes}) - - // create fake client and run CreateAppConfigMaps - fakeCLI := fake.NewClientBuilder().Build() - err = CreateAppConfigMaps(ctx, fakeCLI, airgapReader) - req.NoError(err) - - // ensure that the configmaps created are the ones we expected - allCms := &corev1.ConfigMapList{} - err = fakeCLI.List(ctx, allCms, client.InNamespace("kotsadm")) - req.NoError(err) - req.Equal(len(tt.wantConfigmaps), len(allCms.Items)) - - for _, cm := range tt.wantConfigmaps { - gotCM := corev1.ConfigMap{} - err = fakeCLI.Get(ctx, client.ObjectKey{Namespace: cm.Namespace, Name: cm.Name}, &gotCM) - req.NoError(err) - req.Equal(cm.ObjectMeta.Annotations, gotCM.ObjectMeta.Annotations) - req.Equal(cm.ObjectMeta.Labels, gotCM.ObjectMeta.Labels) - if cm.Data != nil { - req.Equal(cm.Data, gotCM.Data) - } - } - }) - } -} - -func createTarballFromDir(rootPath string, additionalFiles map[string][]byte) io.Reader { - appTarReader, appWriter := io.Pipe() - gWriter := gzip.NewWriter(appWriter) - appTarWriter := tar.NewWriter(gWriter) - go func() { - err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if rootPath == path { - return nil - } - header, err := tar.FileInfoHeader(info, info.Name()) - if err != nil { - return err - } - header.Name = filepath.Base(path) - err = appTarWriter.WriteHeader(header) - if err != nil { - return err - } - if !info.IsDir() { - file, err := os.Open(path) - if err != nil { - return err - } - _, err = io.Copy(appTarWriter, file) - if err != nil { - return err - } - err = file.Close() - if err != nil { - return err - } - } - return nil - }) - if err != nil { - appTarWriter.Close() - appWriter.CloseWithError(err) - return - } - for name, data := range additionalFiles { - header := tar.Header{ - Name: name, - Size: int64(len(data)), - } - err = appTarWriter.WriteHeader(&header) - if err != nil { - appTarWriter.Close() - appWriter.CloseWithError(err) - return - } - _, err = appTarWriter.Write(data) - if err != nil { - appTarWriter.Close() - appWriter.CloseWithError(err) - return - } - } - err = appTarWriter.Close() - if err != nil { - appWriter.CloseWithError(err) - return - } - err = gWriter.Close() - if err != nil { - appWriter.CloseWithError(err) - return - } - err = appWriter.Close() - if err != nil { - return - } - }() - - return appTarReader -} diff --git a/pkg/airgap/materialize.go b/pkg/airgap/materialize.go index 008958694..87a05fc60 100644 --- a/pkg/airgap/materialize.go +++ b/pkg/airgap/materialize.go @@ -13,8 +13,9 @@ import ( const K0sImagePath = "/var/lib/k0s/images/install.tar" -// MaterializeAirgap places the airgap image bundle for k0s -// this should be located at 'images-amd64.tar.gz' within embedded-cluster.tar.gz within the airgap bundle +// MaterializeAirgap places the airgap image bundle for k0s and the embedded cluster charts on disk. +// - image bundle should be located at 'images-amd64.tar' within the embedded-cluster directory within the airgap bundle. +// - charts should be located at 'charts.tar.gz' within the embedded-cluster directory within the airgap bundle. func MaterializeAirgap(airgapReader io.Reader) error { // decompress tarball ungzip, err := gzip.NewReader(airgapReader) diff --git a/pkg/airgap/version_test.go b/pkg/airgap/version_test.go index 94d9aac64..d6caf87d8 100644 --- a/pkg/airgap/version_test.go +++ b/pkg/airgap/version_test.go @@ -1,10 +1,14 @@ package airgap import ( - "github.com/stretchr/testify/require" + "archive/tar" + "compress/gzip" + "io" "os" "path/filepath" "testing" + + "github.com/stretchr/testify/require" ) func TestAirgapBundleVersions(t *testing.T) { @@ -40,3 +44,82 @@ func TestAirgapBundleVersions(t *testing.T) { }) } } + +func createTarballFromDir(rootPath string, additionalFiles map[string][]byte) io.Reader { + appTarReader, appWriter := io.Pipe() + gWriter := gzip.NewWriter(appWriter) + appTarWriter := tar.NewWriter(gWriter) + go func() { + err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if rootPath == path { + return nil + } + header, err := tar.FileInfoHeader(info, info.Name()) + if err != nil { + return err + } + header.Name = filepath.Base(path) + err = appTarWriter.WriteHeader(header) + if err != nil { + return err + } + if !info.IsDir() { + file, err := os.Open(path) + if err != nil { + return err + } + _, err = io.Copy(appTarWriter, file) + if err != nil { + return err + } + err = file.Close() + if err != nil { + return err + } + } + return nil + }) + if err != nil { + appTarWriter.Close() + appWriter.CloseWithError(err) + return + } + for name, data := range additionalFiles { + header := tar.Header{ + Name: name, + Size: int64(len(data)), + } + err = appTarWriter.WriteHeader(&header) + if err != nil { + appTarWriter.Close() + appWriter.CloseWithError(err) + return + } + _, err = appTarWriter.Write(data) + if err != nil { + appTarWriter.Close() + appWriter.CloseWithError(err) + return + } + } + err = appTarWriter.Close() + if err != nil { + appWriter.CloseWithError(err) + return + } + err = gWriter.Close() + if err != nil { + appWriter.CloseWithError(err) + return + } + err = appWriter.Close() + if err != nil { + return + } + }() + + return appTarReader +} diff --git a/pkg/goods/goods.go b/pkg/goods/goods.go index 7e2711426..7df0122ac 100644 --- a/pkg/goods/goods.go +++ b/pkg/goods/goods.go @@ -99,3 +99,30 @@ func MaterializeLocalArtifactMirrorUnitFile() error { } return nil } + +//go:embed internal/bins/* +var internalBinfs embed.FS + +// MaterializeInternalBinary materializes an internal binary from inside internal/bins directory +// and writes it to a tmp file. It returns the path to the materialized binary. +// The binary should be deleted after it is used. +// This is used for binaries that are not meant to be exposed to the user. +func MaterializeInternalBinary(name string) (string, error) { + srcpath := fmt.Sprintf("internal/bins/%s", name) + srcfile, err := internalBinfs.ReadFile(srcpath) + if err != nil { + return "", fmt.Errorf("unable to read asset: %w", err) + } + dstpath, err := os.CreateTemp("", fmt.Sprintf("embedded-cluster-%s-bin-", name)) + if err != nil { + return "", fmt.Errorf("unable to create temp file: %w", err) + } + defer dstpath.Close() + if _, err := dstpath.Write(srcfile); err != nil { + return "", fmt.Errorf("unable to write file: %w", err) + } + if err := dstpath.Chmod(0755); err != nil { + return "", fmt.Errorf("unable to set executable permissions: %w", err) + } + return dstpath.Name(), nil +} diff --git a/pkg/helpers/command.go b/pkg/helpers/command.go new file mode 100644 index 000000000..4a3cd2c73 --- /dev/null +++ b/pkg/helpers/command.go @@ -0,0 +1,32 @@ +package helpers + +import ( + "bytes" + "fmt" + "os/exec" + + "github.com/sirupsen/logrus" +) + +// RunCommand spawns a command and capture its output. Outputs are logged using the +// logrus package and stdout is returned as a string. +func RunCommand(bin string, args ...string) (string, error) { + fullcmd := append([]string{bin}, args...) + logrus.Debugf("running command: %v", fullcmd) + + stdout := bytes.NewBuffer(nil) + stderr := bytes.NewBuffer(nil) + cmd := exec.Command(bin, args...) + cmd.Stdout = stdout + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + logrus.Debugf("failed to run command:") + logrus.Debugf("stdout: %s", stdout.String()) + logrus.Debugf("stderr: %s", stderr.String()) + if stderr.String() != "" { + return "", fmt.Errorf("%w: %s", err, stderr.String()) + } + return "", err + } + return stdout.String(), nil +}