diff --git a/cmds/ocm/commands/controllercmds/install/apply.go b/cmds/ocm/commands/controllercmds/common/applyer.go similarity index 96% rename from cmds/ocm/commands/controllercmds/install/apply.go rename to cmds/ocm/commands/controllercmds/common/applyer.go index 395bb1eea3..a2f07e3966 100644 --- a/cmds/ocm/commands/controllercmds/install/apply.go +++ b/cmds/ocm/commands/controllercmds/common/applyer.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package install +package common import ( "bufio" @@ -20,7 +20,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func readObjects(manifestPath string) ([]*unstructured.Unstructured, error) { +func ReadObjects(manifestPath string) ([]*unstructured.Unstructured, error) { fi, err := os.Lstat(manifestPath) if err != nil { return nil, err diff --git a/cmds/ocm/commands/controllercmds/common/fetcher.go b/cmds/ocm/commands/controllercmds/common/fetcher.go new file mode 100644 index 0000000000..189bcca072 --- /dev/null +++ b/cmds/ocm/commands/controllercmds/common/fetcher.go @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package common + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" +) + +func getLatestVersion(ctx context.Context, url string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url+"/latest", nil) + if err != nil { + return "", err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("GitHub API call failed: %w", err) + } + + if res.Body != nil { + defer res.Body.Close() + } + + type meta struct { + Tag string `json:"tag_name"` + } + var m meta + if err := json.NewDecoder(res.Body).Decode(&m); err != nil { + return "", fmt.Errorf("decoding GitHub API response failed: %w", err) + } + + return m.Tag, err +} + +// existingVersion calls the GitHub API to confirm the given version does exist. +func existingVersion(ctx context.Context, url, version string) (bool, error) { + if !strings.HasPrefix(version, "v") { + version = "v" + version + } + + ghURL := fmt.Sprintf(url+"/tags/%s", version) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, ghURL, nil) + if err != nil { + return false, fmt.Errorf("GitHub API call failed: %w", err) + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return false, err + } + + if res.Body != nil { + defer res.Body.Close() + } + + switch res.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusNotFound: + return false, nil + default: + return false, fmt.Errorf("GitHub API returned an unexpected status code (%d)", res.StatusCode) + } +} + +func fetch(ctx context.Context, url, version, dir, filename string) error { + ghURL := fmt.Sprintf("%s/latest/download/%s", url, filename) + if strings.HasPrefix(version, "v") { + ghURL = fmt.Sprintf("%s/download/%s/%s", url, version, filename) + } + + req, err := http.NewRequest(http.MethodGet, ghURL, nil) + if err != nil { + return fmt.Errorf("failed to create HTTP request for %s, error: %w", ghURL, err) + } + + resp, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return fmt.Errorf("failed to download manifests.tar.gz from %s, error: %w", ghURL, err) + } + defer resp.Body.Close() + + // check response + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download %s from %s, status: %s", filename, ghURL, resp.Status) + } + + wf, err := os.OpenFile(filepath.Join(dir, filename), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o777) + if err != nil { + return fmt.Errorf("failed to open temp file: %w", err) + } + + if _, err := io.Copy(wf, resp.Body); err != nil { + return fmt.Errorf("failed to write to temp file: %w", err) + } + + return nil +} diff --git a/cmds/ocm/commands/controllercmds/common/manifests.go b/cmds/ocm/commands/controllercmds/common/manifests.go new file mode 100644 index 0000000000..b49871b6d4 --- /dev/null +++ b/cmds/ocm/commands/controllercmds/common/manifests.go @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package common + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/fluxcd/pkg/ssa" + "github.com/open-component-model/ocm/pkg/contexts/clictx" + "github.com/open-component-model/ocm/pkg/out" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func Install(ctx context.Context, octx clictx.Context, sm *ssa.ResourceManager, releaseURL, baseURL, manifest, filename, version string, dryRun bool) error { + objects, err := fetchObjects(ctx, octx, releaseURL, baseURL, manifest, filename, version, dryRun) + if err != nil { + return fmt.Errorf("✗ failed to construct objects to apply: %w", err) + } + + // dry run was set to true, no objects are returned + if len(objects) == 0 { + return nil + } + + if _, err := sm.ApplyAllStaged(context.Background(), objects, ssa.DefaultApplyOptions()); err != nil { + return fmt.Errorf("✗ failed to apply manifests: %w", err) + } + + out.Outf(octx, "► waiting for ocm deployment to be ready\n") + if err = sm.Wait(objects, ssa.DefaultWaitOptions()); err != nil { + return fmt.Errorf("✗ failed to wait for objects to be ready: %w", err) + } + + return nil +} + +func Uninstall(ctx context.Context, octx clictx.Context, sm *ssa.ResourceManager, releaseURL, baseURL, manifest, filename, version string, dryRun bool) error { + objects, err := fetchObjects(ctx, octx, releaseURL, baseURL, manifest, filename, version, dryRun) + if err != nil { + return fmt.Errorf("✗ failed to construct objects to apply: %w", err) + } + + // dry run was set to true, no objects are returned + if len(objects) == 0 { + return nil + } + + if _, err := sm.DeleteAll(context.Background(), objects, ssa.DefaultDeleteOptions()); err != nil { + return fmt.Errorf("✗ failed to delete manifests: %w", err) + } + + out.Outf(octx, "► waiting for ocm deployment to be deleted\n") + if err = sm.WaitForTermination(objects, ssa.DefaultWaitOptions()); err != nil { + return fmt.Errorf("✗ failed to wait for objects to be deleted: %w", err) + } + + return nil +} + +func fetchObjects(ctx context.Context, octx clictx.Context, releaseURL, baseURL, manifest, filename, version string, dryRun bool) ([]*unstructured.Unstructured, error) { + if version == "latest" { + latest, err := getLatestVersion(ctx, releaseURL) + if err != nil { + return nil, fmt.Errorf("✗ failed to retrieve latest version for %s: %w", manifest, err) + } + out.Outf(octx, "► got latest version %q\n", latest) + version = latest + } else { + exists, err := existingVersion(ctx, releaseURL, version) + if err != nil { + return nil, fmt.Errorf("✗ failed to check if version exists: %w", err) + } + if !exists { + return nil, fmt.Errorf("✗ version %q does not exist", version) + } + } + + temp, err := os.MkdirTemp("", manifest+"-download") + if err != nil { + return nil, fmt.Errorf("✗ failed to create temp folder: %w", err) + } + defer os.RemoveAll(temp) + + if err := fetch(ctx, baseURL, version, temp, filename); err != nil { + return nil, fmt.Errorf("✗ failed to download install.yaml file: %w", err) + } + + path := filepath.Join(temp, filename) + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, fmt.Errorf("✗ failed to find %s file at location: %w", filename, err) + } + out.Outf(octx, "✔ successfully fetched install file\n") + if dryRun { + content, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("✗ failed to read %s file at location: %w", filename, err) + } + + out.Outf(octx, string(content)) + return nil, nil + } + out.Outf(octx, "► applying to cluster...\n") + + objects, err := ReadObjects(path) + if err != nil { + return nil, fmt.Errorf("✗ failed to construct objects to apply: %w", err) + } + + return objects, nil +} diff --git a/cmds/ocm/commands/controllercmds/install/cmd.go b/cmds/ocm/commands/controllercmds/install/cmd.go index f79063131b..546601e9b5 100644 --- a/cmds/ocm/commands/controllercmds/install/cmd.go +++ b/cmds/ocm/commands/controllercmds/install/cmd.go @@ -6,16 +6,11 @@ package install import ( "context" - "encoding/json" "fmt" - "io" - "net/http" - "os" - "strings" "time" "github.com/fluxcd/pkg/ssa" - "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/open-component-model/ocm/cmds/ocm/commands/controllercmds/common" "github.com/spf13/cobra" "github.com/spf13/pflag" corev1 "k8s.io/api/core/v1" @@ -55,7 +50,7 @@ type Command struct { var _ utils.OCMCommand = (*Command)(nil) -// NewCommand creates a new controller cdommand. +// NewCommand creates a new controller command. func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { return utils.SetupCommand(&Command{BaseCommand: utils.NewBaseCommand(ctx)}, utils.Names(Names, names...)...) } @@ -114,13 +109,16 @@ func (o *Command) Run() error { out.Outf(o.Context, "► installing ocm-controller with version %s\n", o.Version) version := o.Version - if err := o.installManifest( + if err := common.Install( ctx, + o.Context, + sm, o.ReleaseAPIURL, o.BaseURL, "ocm-controller", "install.yaml", version, + o.DryRun, ); err != nil { return err } @@ -129,158 +127,6 @@ func (o *Command) Run() error { return nil } -func (o *Command) installManifest(ctx context.Context, releaseURL, baseURL, manifest, filename, version string) error { - if version == "latest" { - latest, err := o.getLatestVersion(ctx, releaseURL) - if err != nil { - return fmt.Errorf("✗ failed to retrieve latest version for %s: %w", manifest, err) - } - out.Outf(o.Context, "► got latest version %q\n", latest) - version = latest - } else { - exists, err := o.existingVersion(ctx, releaseURL, version) - if err != nil { - return fmt.Errorf("✗ failed to check if version exists: %w", err) - } - if !exists { - return fmt.Errorf("✗ version %q does not exist", version) - } - } - - temp, err := os.MkdirTemp("", manifest+"-download") - if err != nil { - return fmt.Errorf("✗ failed to create temp folder: %w", err) - } - defer os.RemoveAll(temp) - - if err := o.fetch(ctx, baseURL, version, temp, filename); err != nil { - return fmt.Errorf("✗ failed to download install.yaml file: %w", err) - } - - path := filepath.Join(temp, filename) - if _, err := os.Stat(path); os.IsNotExist(err) { - return fmt.Errorf("✗ failed to find %s file at location: %w", filename, err) - } - out.Outf(o.Context, "✔ successfully fetched install file\n") - if o.DryRun { - content, err := os.ReadFile(path) - if err != nil { - return fmt.Errorf("✗ failed to read %s file at location: %w", filename, err) - } - out.Outf(o.Context, string(content)) - return nil - } - out.Outf(o.Context, "► applying to cluster...\n") - - objects, err := readObjects(path) - if err != nil { - return fmt.Errorf("✗ failed to construct objects to apply: %w", err) - } - - if _, err := o.SM.ApplyAllStaged(context.Background(), objects, ssa.DefaultApplyOptions()); err != nil { - return fmt.Errorf("✗ failed to apply manifests: %w", err) - } - - out.Outf(o.Context, "► waiting for ocm deployment to be ready\n") - if err = o.SM.Wait(objects, ssa.DefaultWaitOptions()); err != nil { - return fmt.Errorf("✗ failed to wait for objects to be ready: %w", err) - } - - return nil -} - -// getLatestVersion calls the GitHub API and returns the latest released version. -func (o *Command) getLatestVersion(ctx context.Context, url string) (string, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url+"/latest", nil) - if err != nil { - return "", err - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - return "", fmt.Errorf("GitHub API call failed: %w", err) - } - - if res.Body != nil { - defer res.Body.Close() - } - - type meta struct { - Tag string `json:"tag_name"` - } - var m meta - if err := json.NewDecoder(res.Body).Decode(&m); err != nil { - return "", fmt.Errorf("decoding GitHub API response failed: %w", err) - } - - return m.Tag, err -} - -// existingVersion calls the GitHub API to confirm the given version does exist. -func (o *Command) existingVersion(ctx context.Context, url, version string) (bool, error) { - if !strings.HasPrefix(version, "v") { - version = "v" + version - } - - ghURL := fmt.Sprintf(url+"/tags/%s", version) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, ghURL, nil) - if err != nil { - return false, fmt.Errorf("GitHub API call failed: %w", err) - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - return false, err - } - - if res.Body != nil { - defer res.Body.Close() - } - - switch res.StatusCode { - case http.StatusOK: - return true, nil - case http.StatusNotFound: - return false, nil - default: - return false, fmt.Errorf("GitHub API returned an unexpected status code (%d)", res.StatusCode) - } -} - -func (o *Command) fetch(ctx context.Context, url, version, dir, filename string) error { - ghURL := fmt.Sprintf("%s/latest/download/%s", url, filename) - if strings.HasPrefix(version, "v") { - ghURL = fmt.Sprintf("%s/download/%s/%s", url, version, filename) - } - - req, err := http.NewRequest(http.MethodGet, ghURL, nil) - if err != nil { - return fmt.Errorf("failed to create HTTP request for %s, error: %w", ghURL, err) - } - - resp, err := http.DefaultClient.Do(req.WithContext(ctx)) - if err != nil { - return fmt.Errorf("failed to download manifests.tar.gz from %s, error: %w", ghURL, err) - } - defer resp.Body.Close() - - // check response - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to download %s from %s, status: %s", filename, ghURL, resp.Status) - } - - wf, err := os.OpenFile(filepath.Join(dir, filename), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o777) - if err != nil { - return fmt.Errorf("failed to open temp file: %w", err) - } - - if _, err := io.Copy(wf, resp.Body); err != nil { - return fmt.Errorf("failed to write to temp file: %w", err) - } - - return nil -} - // RunPreFlightCheck checks if the target cluster has the following items: // - secret containing certificates for the in-cluster registry // - flux installed. diff --git a/cmds/ocm/commands/controllercmds/install/install_cert_manager.go b/cmds/ocm/commands/controllercmds/install/install_cert_manager.go index e24766d770..50c87df916 100644 --- a/cmds/ocm/commands/controllercmds/install/install_cert_manager.go +++ b/cmds/ocm/commands/controllercmds/install/install_cert_manager.go @@ -8,6 +8,7 @@ import ( "github.com/fluxcd/pkg/ssa" "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/open-component-model/ocm/cmds/ocm/commands/controllercmds/common" "github.com/open-component-model/ocm/pkg/out" ) @@ -17,15 +18,7 @@ var issuer []byte func (o *Command) installPrerequisites(ctx context.Context) error { out.Outf(o.Context, "► installing cert-manager with version %s\n", o.CertManagerVersion) - version := o.CertManagerVersion - if err := o.installManifest( - ctx, - o.CertManagerReleaseAPIURL, - o.CertManagerBaseURL, - "cert-manager", - "cert-manager.yaml", - version, - ); err != nil { + if err := common.Install(ctx, o.Context, o.SM, o.CertManagerReleaseAPIURL, o.CertManagerBaseURL, "cert-manager", "cert-manager.yaml", o.CertManagerVersion, o.DryRun); err != nil { return err } @@ -51,7 +44,7 @@ func (o *Command) createRegistryCertificate() error { return fmt.Errorf("failed to write issuer.yaml file at location: %w", err) } - objects, err := readObjects(path) + objects, err := common.ReadObjects(path) if err != nil { return fmt.Errorf("failed to construct objects to apply: %w", err) } diff --git a/cmds/ocm/commands/controllercmds/install/resource_manager.go b/cmds/ocm/commands/controllercmds/install/resource_manager.go new file mode 100644 index 0000000000..ffd7cde1be --- /dev/null +++ b/cmds/ocm/commands/controllercmds/install/resource_manager.go @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package install + +import ( + "fmt" + + "github.com/fluxcd/cli-utils/pkg/kstatus/polling" + "github.com/fluxcd/pkg/ssa" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// ownerRef contains the server-side apply field manager and ownership labels group. +var ownerRef = ssa.Owner{ + Field: "ocm", + Group: "ocm-controller.delivery.ocm.software", +} + +// NewResourceManager creates a ResourceManager for the given cluster. +func NewResourceManager(rcg genericclioptions.RESTClientGetter) (*ssa.ResourceManager, error) { + cfg, err := rcg.ToRESTConfig() + if err != nil { + return nil, fmt.Errorf("loading kubeconfig failed: %w", err) + } + // bump limits + cfg.QPS = 100.0 + cfg.Burst = 300 + + restMapper, err := rcg.ToRESTMapper() + if err != nil { + return nil, err + } + + kubeClient, err := client.New(cfg, client.Options{Mapper: restMapper, Scheme: newScheme()}) + if err != nil { + return nil, err + } + + kubePoller := polling.NewStatusPoller(kubeClient, restMapper, polling.Options{}) + + return ssa.NewResourceManager(kubeClient, kubePoller, ownerRef), nil +} + +func newScheme() *apiruntime.Scheme { + scheme := apiruntime.NewScheme() + _ = apiextensionsv1.AddToScheme(scheme) + _ = corev1.AddToScheme(scheme) + return scheme +} diff --git a/cmds/ocm/commands/controllercmds/uninstall/cmd.go b/cmds/ocm/commands/controllercmds/uninstall/cmd.go new file mode 100644 index 0000000000..e8ff182a46 --- /dev/null +++ b/cmds/ocm/commands/controllercmds/uninstall/cmd.go @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package uninstall + +import ( + "context" + "fmt" + "time" + + "github.com/fluxcd/pkg/ssa" + "github.com/open-component-model/ocm/cmds/ocm/commands/controllercmds/common" + "github.com/open-component-model/ocm/cmds/ocm/commands/controllercmds/names" + "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" + "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" + "github.com/open-component-model/ocm/pkg/contexts/clictx" + "github.com/open-component-model/ocm/pkg/out" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +var ( + Names = names.Controller + Verb = verbs.Uninstall +) + +type Command struct { + utils.BaseCommand + Namespace string + ControllerName string + Timeout time.Duration + Version string + BaseURL string + ReleaseAPIURL string + CertManagerBaseURL string + CertManagerReleaseAPIURL string + CertManagerVersion string + SM *ssa.ResourceManager + UninstallPrerequisites bool + DryRun bool +} + +var _ utils.OCMCommand = (*Command)(nil) + +// NewCommand creates a new controller command. +func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { + return utils.SetupCommand(&Command{BaseCommand: utils.NewBaseCommand(ctx)}, utils.Names(Names, names...)...) +} + +func (o *Command) ForName(name string) *cobra.Command { + return &cobra.Command{ + Use: "uninstall controller", + Short: "Uninstalls the ocm-controller and all of its dependencies", + } +} + +// AddFlags for the known item to delete. +func (o *Command) AddFlags(set *pflag.FlagSet) { + set.StringVarP(&o.Version, "version", "v", "latest", "the version of the controller to install") + set.StringVarP(&o.BaseURL, "base-url", "u", "https://github.com/open-component-model/ocm-controller/releases", "the base url to the ocm-controller's release page") + set.StringVarP(&o.ReleaseAPIURL, "release-api-url", "a", "https://api.github.com/repos/open-component-model/ocm-controller/releases", "the base url to the ocm-controller's API release page") + set.StringVar(&o.CertManagerBaseURL, "cert-manager-base-url", "https://github.com/cert-manager/cert-manager/releases", "the base url to the cert-manager's release page") + set.StringVar(&o.CertManagerReleaseAPIURL, "cert-manager-release-api-url", "https://api.github.com/repos/cert-manager/cert-manager/releases", "the base url to the cert-manager's API release page") + set.StringVar(&o.CertManagerVersion, "cert-manager-version", "v1.13.2", "version for cert-manager") + set.StringVarP(&o.ControllerName, "controller-name", "c", "ocm-controller", "name of the controller that's used for status check") + set.StringVarP(&o.Namespace, "namespace", "n", "ocm-system", "the namespace into which the controller is installed") + set.DurationVarP(&o.Timeout, "timeout", "t", 1*time.Minute, "maximum time to wait for deployment to be ready") + set.BoolVarP(&o.UninstallPrerequisites, "uninstall-prerequisites", "p", false, "uninstall prerequisites required by ocm-controller") + set.BoolVarP(&o.DryRun, "dry-run", "d", false, "if enabled, prints the downloaded manifest file") +} + +func (o *Command) Complete(args []string) error { + return nil +} + +func (o *Command) Run() error { + kubeconfigArgs := genericclioptions.NewConfigFlags(false) + sm, err := NewResourceManager(kubeconfigArgs) + if err != nil { + return fmt.Errorf("✗ failed to create resource manager: %w", err) + } + + o.SM = sm + ctx := context.Background() + + out.Outf(o.Context, "► uninstalling ocm-controller with version %s\n", o.Version) + if err := common.Uninstall( + ctx, + o.Context, + sm, + o.ReleaseAPIURL, + o.BaseURL, + "ocm-controller", + "install.yaml", + o.Version, + o.DryRun, + ); err != nil { + return err + } + + out.Outf(o.Context, "✔ ocm-controller successfully uninstalled\n") + + if o.UninstallPrerequisites { + out.Outf(o.Context, "► uninstalling cert-manager and issuers\n") + if err := o.uninstallPrerequisites(ctx); err != nil { + return fmt.Errorf("✗ failed to uninstall pre-requesits: %w\n", err) + } + + out.Outf(o.Context, "✔ successfully uninstalled prerequisites\n") + } + + return nil +} diff --git a/cmds/ocm/commands/controllercmds/uninstall/cmd_test.go b/cmds/ocm/commands/controllercmds/uninstall/cmd_test.go new file mode 100644 index 0000000000..b123b5bda3 --- /dev/null +++ b/cmds/ocm/commands/controllercmds/uninstall/cmd_test.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package uninstall_test + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/cmds/ocm/testhelper" + . "github.com/open-component-model/ocm/pkg/testutils" + + "github.com/mandelsoft/filepath/pkg/filepath" +) + +var _ = Describe("Test Environment", func() { + var ( + env *TestEnv + testServer *httptest.Server + ) + + BeforeEach(func() { + env = NewTestEnv() + testServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.String(), "download") { + content, err := os.ReadFile(filepath.Join("testdata", "install.yaml")) + if err != nil { + fmt.Fprintf(w, "failed") + return + } + + fmt.Fprintf(w, string(content)) + return + } + + fmt.Fprintf(w, `{ + "tag_name": "v0.0.1-test" +} +`) + })) + }) + + AfterEach(func() { + env.Cleanup() + }) + + It("uninstall latest version", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("controller", "uninstall", "-d", "-u", testServer.URL, "-a", testServer.URL)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext(`► uninstalling ocm-controller with version latest +► got latest version "v0.0.1-test" +✔ successfully fetched install file +test: content +✔ ocm-controller successfully uninstalled +`)) + }) + + It("uninstall specific version", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("controller", "uninstall", "-d", "-u", testServer.URL, "-a", testServer.URL, "-v", "v0.1.0-test-2")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext(`► uninstalling ocm-controller with version v0.1.0-test-2 +✔ successfully fetched install file +test: content +✔ ocm-controller successfully uninstalled +`)) + }) +}) diff --git a/cmds/ocm/commands/controllercmds/uninstall/issuer/registry_certificate.yaml b/cmds/ocm/commands/controllercmds/uninstall/issuer/registry_certificate.yaml new file mode 100644 index 0000000000..a19e12f5b5 --- /dev/null +++ b/cmds/ocm/commands/controllercmds/uninstall/issuer/registry_certificate.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ocm-system +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: ocm-issuer +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: ocm-registry-certificate + namespace: ocm-system +spec: + isCA: true + secretName: ocm-registry-tls-certs + dnsNames: + - registry.ocm-system.svc.cluster.local + privateKey: + algorithm: ECDSA + size: 256 + issuerRef: + name: ocm-issuer + kind: ClusterIssuer + group: cert-manager.io diff --git a/cmds/ocm/commands/controllercmds/uninstall/resource_manager.go b/cmds/ocm/commands/controllercmds/uninstall/resource_manager.go new file mode 100644 index 0000000000..0c1dba937a --- /dev/null +++ b/cmds/ocm/commands/controllercmds/uninstall/resource_manager.go @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package uninstall + +import ( + "fmt" + + "github.com/fluxcd/cli-utils/pkg/kstatus/polling" + "github.com/fluxcd/pkg/ssa" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// ownerRef contains the server-side apply field manager and ownership labels group. +var ownerRef = ssa.Owner{ + Field: "ocm", + Group: "ocm-controller.delivery.ocm.software", +} + +// NewResourceManager creates a ResourceManager for the given cluster. +func NewResourceManager(rcg genericclioptions.RESTClientGetter) (*ssa.ResourceManager, error) { + cfg, err := rcg.ToRESTConfig() + if err != nil { + return nil, fmt.Errorf("loading kubeconfig failed: %w", err) + } + // bump limits + cfg.QPS = 100.0 + cfg.Burst = 300 + + restMapper, err := rcg.ToRESTMapper() + if err != nil { + return nil, err + } + + kubeClient, err := client.New(cfg, client.Options{Mapper: restMapper, Scheme: newScheme()}) + if err != nil { + return nil, err + } + + kubePoller := polling.NewStatusPoller(kubeClient, restMapper, polling.Options{}) + + return ssa.NewResourceManager(kubeClient, kubePoller, ownerRef), nil +} + +func newScheme() *apiruntime.Scheme { + scheme := apiruntime.NewScheme() + _ = apiextensionsv1.AddToScheme(scheme) + _ = corev1.AddToScheme(scheme) + return scheme +} diff --git a/cmds/ocm/commands/controllercmds/uninstall/suite_test.go b/cmds/ocm/commands/controllercmds/uninstall/suite_test.go new file mode 100644 index 0000000000..0c28f00a4f --- /dev/null +++ b/cmds/ocm/commands/controllercmds/uninstall/suite_test.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package uninstall_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Uninstall controller") +} diff --git a/cmds/ocm/commands/controllercmds/uninstall/testdata/install.yaml b/cmds/ocm/commands/controllercmds/uninstall/testdata/install.yaml new file mode 100644 index 0000000000..3571bf35c7 --- /dev/null +++ b/cmds/ocm/commands/controllercmds/uninstall/testdata/install.yaml @@ -0,0 +1 @@ +test: content diff --git a/cmds/ocm/commands/controllercmds/uninstall/uninstall_cert_manager.go b/cmds/ocm/commands/controllercmds/uninstall/uninstall_cert_manager.go new file mode 100644 index 0000000000..944c881eb0 --- /dev/null +++ b/cmds/ocm/commands/controllercmds/uninstall/uninstall_cert_manager.go @@ -0,0 +1,68 @@ +package uninstall + +import ( + "context" + _ "embed" + "fmt" + "os" + + "github.com/fluxcd/pkg/ssa" + "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/open-component-model/ocm/cmds/ocm/commands/controllercmds/common" + "github.com/open-component-model/ocm/pkg/out" +) + +//go:embed issuer/registry_certificate.yaml +var issuer []byte + +func (o *Command) uninstallPrerequisites(ctx context.Context) error { + out.Outf(o.Context, "► uninstalling cert-manager with version %s\n", o.CertManagerVersion) + + if err := o.removeRegistryCertificate(); err != nil { + return fmt.Errorf("✗ failed to create registry certificate: %w", err) + } + + version := o.CertManagerVersion + if err := common.Uninstall( + ctx, + o.Context, + o.SM, + o.CertManagerReleaseAPIURL, + o.CertManagerBaseURL, + "cert-manager", + "cert-manager.yaml", + version, + o.DryRun, + ); err != nil { + return err + } + + out.Outf(o.Context, "✔ cert-manager successfully uninstalled\n") + + return nil +} + +func (o *Command) removeRegistryCertificate() error { + out.Outf(o.Context, "► remove certificate for internal registry\n") + temp, err := os.MkdirTemp("", "issuer") + if err != nil { + return fmt.Errorf("failed to create temp folder: %w", err) + } + defer os.RemoveAll(temp) + + path := filepath.Join(temp, "issuer.yaml") + if err := os.WriteFile(path, issuer, 0o600); err != nil { + return fmt.Errorf("failed to write issuer.yaml file at location: %w", err) + } + + objects, err := common.ReadObjects(path) + if err != nil { + return fmt.Errorf("failed to construct objects to apply: %w", err) + } + + if _, err := o.SM.DeleteAll(context.Background(), objects, ssa.DefaultDeleteOptions()); err != nil { + return fmt.Errorf("failed to delete manifests: %w", err) + } + + return nil +} diff --git a/cmds/ocm/commands/verbs/controller/cmd.go b/cmds/ocm/commands/verbs/controller/cmd.go index f93b0d7f95..3575fb9988 100644 --- a/cmds/ocm/commands/verbs/controller/cmd.go +++ b/cmds/ocm/commands/verbs/controller/cmd.go @@ -5,6 +5,7 @@ package controller import ( + "github.com/open-component-model/ocm/cmds/ocm/commands/controllercmds/uninstall" "github.com/spf13/cobra" "github.com/open-component-model/ocm/cmds/ocm/commands/controllercmds/install" @@ -19,5 +20,6 @@ func NewCommand(ctx clictx.Context) *cobra.Command { Short: "Commands acting on the ocm-controller", }, names.Controller...) cmd.AddCommand(install.NewCommand(ctx, install.Verb)) + cmd.AddCommand(uninstall.NewCommand(ctx, uninstall.Verb)) return cmd } diff --git a/cmds/ocm/commands/verbs/verbs.go b/cmds/ocm/commands/verbs/verbs.go index 636e929676..489ae83c91 100644 --- a/cmds/ocm/commands/verbs/verbs.go +++ b/cmds/ocm/commands/verbs/verbs.go @@ -20,5 +20,6 @@ const ( Verify = "verify" Clean = "clean" Install = "install" + Uninstall = "uninstall" Execute = "execute" )