diff --git a/.github/workflows/semantic-pr.yaml b/.github/workflows/semantic-pr.yaml index 3b1609cee..c7392ef0f 100644 --- a/.github/workflows/semantic-pr.yaml +++ b/.github/workflows/semantic-pr.yaml @@ -14,6 +14,6 @@ jobs: semantic-message: runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@e32d7e603df1aa1ba07e981f2a23455dee596825 # v5 + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e1a244283..f7014c353 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.10.1" + ".": "0.11.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0075637..2c914cdbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [0.11.0](https://github.com/glasskube/glasskube/compare/v0.10.1...v0.11.0) (2024-06-27) + + +### Features + +* **cli:** add ascii art on glasskube version ([#879](https://github.com/glasskube/glasskube/issues/879)) ([3040ab1](https://github.com/glasskube/glasskube/commit/3040ab10f156a551c1bcbbaa06c79e819460dad3)) + + +### Bug Fixes + +* **cli:** standardize usage texts ([#848](https://github.com/glasskube/glasskube/issues/848)) ([7d23c1e](https://github.com/glasskube/glasskube/commit/7d23c1e638c406e827a25388a56efd0707eeecac)) +* **deps:** update module github.com/yuin/goldmark to v1.7.4 ([#868](https://github.com/glasskube/glasskube/issues/868)) ([d7ce5fa](https://github.com/glasskube/glasskube/commit/d7ce5fa72434e93720a9681279e8ad3e5e058cfe)) +* **open:** fix typo in service name candidate ([#885](https://github.com/glasskube/glasskube/issues/885)) ([921d049](https://github.com/glasskube/glasskube/commit/921d049ff4f3575ee863a1f2ed3f5b78ea94bf47)) + + +### Other + +* **website:** configure eslint with docusaurus, react-ts and prettier plugins ([#858](https://github.com/glasskube/glasskube/issues/858)) ([613cbb7](https://github.com/glasskube/glasskube/commit/613cbb728da7cd1329b75b3148b17c2cb01ea50b)) + + +### Docs + +* exchange static image with gif ([#862](https://github.com/glasskube/glasskube/issues/862)) ([946baf4](https://github.com/glasskube/glasskube/commit/946baf46f4872ed2b45188dfb378ed0f2df6cb24)) +* **website:** exchange repo mockup with actual screenshots ([#852](https://github.com/glasskube/glasskube/issues/852)) ([8adf8fb](https://github.com/glasskube/glasskube/commit/8adf8fb8e20f29e635eb9ce812338dd068f297bb)) +* **website:** fix broken link ([#886](https://github.com/glasskube/glasskube/issues/886)) ([146dc25](https://github.com/glasskube/glasskube/commit/146dc25b11771cb81aa782fa9ec4895bccdd4a07)) +* **website:** fix typo ([#878](https://github.com/glasskube/glasskube/issues/878)) ([e6ebb8c](https://github.com/glasskube/glasskube/commit/e6ebb8c16b3f41976e41b86ed5d4d130ed80fa32)) +* **website:** glasskube is backed by Y Combinator ([#853](https://github.com/glasskube/glasskube/issues/853)) ([05e2ef7](https://github.com/glasskube/glasskube/commit/05e2ef7ce37af1ee31618dbb49258ab45d3a8a37)) + ## [0.10.1](https://github.com/glasskube/glasskube/compare/v0.10.0...v0.10.1) (2024-06-24) diff --git a/api/v1alpha1/package_manifest.go b/api/v1alpha1/package_manifest.go index c26a1e077..2360674bd 100644 --- a/api/v1alpha1/package_manifest.go +++ b/api/v1alpha1/package_manifest.go @@ -85,6 +85,14 @@ func (PackageScope) JSONSchema() *jsonschema.Schema { } } +func (s *PackageScope) IsCluster() bool { + return s == nil || *s == ScopeCluster +} + +func (s *PackageScope) IsNamespaced() bool { + return s != nil && *s == ScopeNamespaced +} + type PackageManifest struct { // Scope is optional (default is Cluster) Scope *PackageScope `json:"scope,omitempty"` diff --git a/cmd/glasskube/cmd/auto-update.go b/cmd/glasskube/cmd/auto-update.go index f72451d2f..c2b0c7bb0 100644 --- a/cmd/glasskube/cmd/auto-update.go +++ b/cmd/glasskube/cmd/auto-update.go @@ -8,17 +8,24 @@ import ( "github.com/glasskube/glasskube/api/v1alpha1" "github.com/glasskube/glasskube/internal/cliutils" "github.com/glasskube/glasskube/internal/config" + "github.com/glasskube/glasskube/internal/controller/ctrlpkg" "github.com/glasskube/glasskube/pkg/statuswriter" "github.com/glasskube/glasskube/pkg/update" "github.com/spf13/cobra" "go.uber.org/multierr" ) -var autoUpdateEnabledDisabledOptions = struct{ Yes, All bool }{} +var autoUpdateEnabledDisabledOptions = struct { + Yes, All bool + KindOptions + NamespaceOptions +}{ + KindOptions: DefaultKindOptions(), +} var autoUpdateEnableCmd = &cobra.Command{ Use: "enable [...package]", - Short: "Enable automatic updates for packages", + Short: "Enable automatic updates for packages:", PreRun: cliutils.SetupClientContext(true, &rootCmdOptions.SkipUpdateCheck), ValidArgsFunction: completeInstalledPackageNames, Run: runAutoUpdateEnableOrDisable(true, @@ -27,8 +34,8 @@ var autoUpdateEnableCmd = &cobra.Command{ var autoUpdateDisableCmd = &cobra.Command{ Use: "disable [...package]", - Short: "Disable automatic updates for packages", - PreRun: cliutils.SetupClientContext(true, &rootCmdOptions.SkipUpdateCheck), + Short: "Disable automatic updates for packages:", + PreRun: cliutils.SetupClientContext(false, &rootCmdOptions.SkipUpdateCheck), ValidArgsFunction: completeInstalledPackageNames, Run: runAutoUpdateEnableOrDisable(false, "Enable automatic updates for the following packages", "Automatic updates disabled"), @@ -38,30 +45,47 @@ func runAutoUpdateEnableOrDisable(enabled bool, confirmMsg, successMsg string) f return func(cmd *cobra.Command, args []string) { ctx := cmd.Context() client := cliutils.PackageClient(ctx) - var pkgs []v1alpha1.ClusterPackage + var pkgs []ctrlpkg.Package if autoUpdateEnabledDisabledOptions.All { if len(args) > 0 { fmt.Fprintf(os.Stderr, "Too many arguments: %v\n", args) cliutils.ExitWithError() } - var pkgList v1alpha1.ClusterPackageList - if err := client.ClusterPackages().GetAll(ctx, &pkgList); err != nil { - fmt.Fprintf(os.Stderr, "Could not list packages: %v", err) - cliutils.ExitWithError() + if autoUpdateEnabledDisabledOptions.Kind != KindPackage && autoUpdateEnabledDisabledOptions.Namespace == "" { + var pkgList v1alpha1.ClusterPackageList + if err := client.ClusterPackages().GetAll(ctx, &pkgList); err != nil { + fmt.Fprintf(os.Stderr, "Could not list packages: %v", err) + cliutils.ExitWithError() + } + for i := range pkgList.Items { + pkgs = append(pkgs, &pkgList.Items[i]) + } + } + if autoUpdateEnabledDisabledOptions.Kind != KindClusterPackage { + var pkgList v1alpha1.PackageList + if err := client.Packages(autoUpdateEnabledDisabledOptions.Namespace). + GetAll(ctx, &pkgList); err != nil { + fmt.Fprintf(os.Stderr, "Could not list packages: %v", err) + cliutils.ExitWithError() + } + for i := range pkgList.Items { + pkgs = append(pkgs, &pkgList.Items[i]) + } } - pkgs = pkgList.Items for _, pkg := range pkgs { - args = append(args, pkg.Name) + args = append(args, pkg.GetName()) } } else { if len(args) == 0 { fmt.Fprintln(os.Stderr, "Please specify at least one package") cliutils.ExitWithError() } - pkgs = make([]v1alpha1.ClusterPackage, len(args)) + pkgs = make([]ctrlpkg.Package, len(args)) for i, name := range args { - var pkg v1alpha1.ClusterPackage - if err := client.ClusterPackages().Get(ctx, name, &pkg); err != nil { + pkg, err := getPackageOrClusterPackage(ctx, name, + autoUpdateEnabledDisabledOptions.KindOptions, + autoUpdateEnabledDisabledOptions.NamespaceOptions) + if err != nil { fmt.Fprintf(os.Stderr, "Could not get package %v: %v", name, err) cliutils.ExitWithError() } @@ -69,7 +93,20 @@ func runAutoUpdateEnableOrDisable(enabled bool, confirmMsg, successMsg string) f } } - fmt.Fprintf(os.Stderr, "%v: %v\n", confirmMsg, strings.Join(args, ", ")) + if len(pkgs) == 0 { + fmt.Fprintln(os.Stderr, "Nothing to do") + cliutils.ExitSuccess() + } + + fmt.Fprintln(os.Stderr, confirmMsg) + for _, pkg := range pkgs { + if pkg.IsNamespaceScoped() { + fmt.Fprintf(os.Stderr, " * %v (Package in namespace %v with type %v)\n", + pkg.GetName(), pkg.GetNamespace(), pkg.GetSpec().PackageInfo.Name) + } else { + fmt.Fprintf(os.Stderr, " * %v (ClusterPackage)\n", pkg.GetName()) + } + } if !autoUpdateEnabledDisabledOptions.Yes && !cliutils.YesNoPrompt("Continue?", true) { cancel() } @@ -78,7 +115,14 @@ func runAutoUpdateEnableOrDisable(enabled bool, confirmMsg, successMsg string) f for _, pkg := range pkgs { if pkg.AutoUpdatesEnabled() != enabled { pkg.SetAutoUpdatesEnabled(enabled) - multierr.AppendInto(&err, client.ClusterPackages().Update(ctx, &pkg)) + switch pkg := pkg.(type) { + case *v1alpha1.ClusterPackage: + multierr.AppendInto(&err, client.ClusterPackages().Update(ctx, pkg)) + case *v1alpha1.Package: + multierr.AppendInto(&err, client.Packages(pkg.Namespace).Update(ctx, pkg)) + default: + panic("unexpected type") + } } } if err != nil { @@ -107,36 +151,49 @@ func runAutoUpdate(cmd *cobra.Command, args []string) { updater := update.NewUpdater(ctx). WithStatusWriter(statuswriter.Stderr()) - var pkgs v1alpha1.ClusterPackageList - if err := client.ClusterPackages().GetAll(ctx, &pkgs); err != nil { + var pkgs []ctrlpkg.Package + + var cpkgList v1alpha1.ClusterPackageList + if err := client.ClusterPackages().GetAll(ctx, &cpkgList); err != nil { + panic(err) + } + + for i, pkg := range cpkgList.Items { + if pkg.AutoUpdatesEnabled() { + pkgs = append(pkgs, &cpkgList.Items[i]) + } + } + + var pkgList v1alpha1.PackageList + if err := client.Packages("").GetAll(ctx, &pkgList); err != nil { panic(err) } - var packageNames []string - for _, pkg := range pkgs.Items { + for i, pkg := range pkgList.Items { if pkg.AutoUpdatesEnabled() { - packageNames = append(packageNames, pkg.Name) + pkgs = append(pkgs, &pkgList.Items[i]) } } - if len(packageNames) == 0 { + + if len(pkgs) == 0 { fmt.Fprintln(os.Stderr, "Automatic updates must be enabled for at least one package") cliutils.ExitSuccess() } - tx, err := updater.Prepare(ctx, packageNames) + tx, err := updater.Prepare(ctx, update.GetExact(pkgs)) if err != nil { fmt.Fprintf(os.Stderr, "Error preparing update: %v\n", err) cliutils.ExitWithError() } printTransaction(*tx) - if updated, err := updater.Apply(ctx, tx); err != nil { + if updated, err := updater.ApplyBlocking(ctx, tx); err != nil { fmt.Fprintf(os.Stderr, "Error applying update: %v\n", err) cliutils.ExitWithError() } else { updatedNames := make([]string, len(updated)) for i := range updated { - updatedNames[i] = updated[i].Name + updatedNames[i] = updated[i].GetName() } fmt.Fprintf(os.Stderr, "Updated packages: %v\n", strings.Join(updatedNames, ", ")) } @@ -150,6 +207,8 @@ func init() { autoUpdateEnabledDisabledOptions.Yes, "do not ask for confirmation") cmd.Flags().BoolVar(&autoUpdateEnabledDisabledOptions.All, "all", autoUpdateEnabledDisabledOptions.All, "set for all packages") + autoUpdateEnabledDisabledOptions.KindOptions.AddFlagsToCommand(cmd) + autoUpdateEnabledDisabledOptions.NamespaceOptions.AddFlagsToCommand(cmd) autoUpdateCmd.AddCommand(cmd) } RootCmd.AddCommand(autoUpdateCmd) diff --git a/cmd/glasskube/cmd/bootstrap.go b/cmd/glasskube/cmd/bootstrap.go index e6866796a..14aa408a1 100644 --- a/cmd/glasskube/cmd/bootstrap.go +++ b/cmd/glasskube/cmd/bootstrap.go @@ -29,6 +29,7 @@ type bootstrapOptions struct { force bool createDefaultRepository bool yes bool + dryRun bool OutputOptions } @@ -76,6 +77,12 @@ var bootstrapCmd = &cobra.Command{ } } + if bootstrapCmdOptions.dryRun { + fmt.Fprintln(os.Stderr, + "šŸ”Ž Dry-run mode is enabled. "+ + "Nothing will be changed in your cluster, but validations will still be run.") + } + verifyLegalUpdate(ctx, installedVersion, targetVersion) manifests, err := client.Bootstrap(ctx, bootstrapCmdOptions.asBootstrapOptions()) @@ -83,7 +90,7 @@ var bootstrapCmd = &cobra.Command{ fmt.Fprintf(os.Stderr, "\nAn error occurred during bootstrap:\n%v\n", err) cliutils.ExitWithError() } - if err := printBootsrap( + if err := printBootstrap( manifests, bootstrapCmdOptions.Output, ); err != nil { @@ -101,10 +108,11 @@ func (o bootstrapOptions) asBootstrapOptions() bootstrap.BootstrapOptions { DisableTelemetry: o.disableTelemetry, Force: o.force, CreateDefaultRepository: o.createDefaultRepository, + DryRun: o.dryRun, } } -func printBootsrap(manifests []unstructured.Unstructured, output OutputFormat) error { +func printBootstrap(manifests []unstructured.Unstructured, output OutputFormat) error { if output != "" { if err := convertAndPrintManifests(manifests, output); err != nil { return err @@ -221,6 +229,8 @@ func init() { bootstrapCmd.Flags().BoolVar(&bootstrapCmdOptions.createDefaultRepository, "create-default-repository", bootstrapCmdOptions.createDefaultRepository, "Toggle creation of the default glasskube package repository") + bootstrapCmd.PersistentFlags().BoolVar(&bootstrapCmdOptions.dryRun, "dry-run", false, + "Do not make any changes but run all validations") bootstrapCmd.Flags().BoolVar(&bootstrapCmdOptions.yes, "yes", false, "Skip confirmation prompt") bootstrapCmd.MarkFlagsMutuallyExclusive("url", "type") bootstrapCmd.MarkFlagsMutuallyExclusive("url", "latest") diff --git a/cmd/glasskube/cmd/configure.go b/cmd/glasskube/cmd/configure.go index 35fd16994..9b15b7a4f 100644 --- a/cmd/glasskube/cmd/configure.go +++ b/cmd/glasskube/cmd/configure.go @@ -27,7 +27,7 @@ var configureCmdOptions = struct { } var configureCmd = &cobra.Command{ - Use: "configure [package-name]", + Use: "configure ", Short: "Configure a package", Args: cobra.ExactArgs(1), PreRun: cliutils.SetupClientContext(true, &rootCmdOptions.SkipUpdateCheck), @@ -132,8 +132,7 @@ func runConfigure(cmd *cobra.Command, args []string) { func init() { configureCmdOptions.ValuesOptions.AddFlagsToCommand(configureCmd) configureCmdOptions.OutputOptions.AddFlagsToCommand(configureCmd) - // TODO: Enable these flags to support namespaced packages - // configureCmdOptions.NamespaceOptions.AddFlagsToCommand(configureCmd) - // configureCmdOptions.KindOptions.AddFlagsToCommand(configureCmd) + configureCmdOptions.NamespaceOptions.AddFlagsToCommand(configureCmd) + configureCmdOptions.KindOptions.AddFlagsToCommand(configureCmd) RootCmd.AddCommand(configureCmd) } diff --git a/cmd/glasskube/cmd/describe.go b/cmd/glasskube/cmd/describe.go index 803d6df7d..0719ae150 100644 --- a/cmd/glasskube/cmd/describe.go +++ b/cmd/glasskube/cmd/describe.go @@ -13,6 +13,7 @@ import ( "github.com/glasskube/glasskube/api/v1alpha1" "github.com/glasskube/glasskube/internal/clientutils" "github.com/glasskube/glasskube/internal/cliutils" + "github.com/glasskube/glasskube/internal/controller/ctrlpkg" "github.com/glasskube/glasskube/internal/manifestvalues" repoclient "github.com/glasskube/glasskube/internal/repo/client" "github.com/glasskube/glasskube/internal/semver" @@ -23,17 +24,21 @@ import ( "github.com/yuin/goldmark" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/util" - apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/yaml" ) var describeCmdOptions = struct { repository string OutputOptions -}{} + KindOptions + NamespaceOptions +}{ + KindOptions: DefaultKindOptions(), +} var describeCmd = &cobra.Command{ - Use: "describe [package-name]", + Use: "describe ", Short: "Describe a package", Long: "Shows additional information about the given package.", Args: cobra.ExactArgs(1), @@ -44,41 +49,104 @@ var describeCmd = &cobra.Command{ pkgName := args[0] repoClient := cliutils.RepositoryClientset(ctx) - latestManifest, latestVersion, err := + latestManifest, latestVersion, lvErr := describe.DescribeLatestVersion(ctx, describeCmdOptions.repository, pkgName) - if err != nil { - fmt.Fprintf(os.Stderr, "āŒ Could not get latest info for %v: %v\n", pkgName, err) - cliutils.ExitWithError() + pkg, pkgErr := + getPackageOrClusterPackage(ctx, pkgName, describeCmdOptions.KindOptions, describeCmdOptions.NamespaceOptions) + + var manifest *v1alpha1.PackageManifest + var err error + + if pkgErr != nil { + if errors.IsNotFound(pkgErr) { + // package not installed -> use latest manifest from repo + if lvErr != nil { + fmt.Fprintf(os.Stderr, "āŒ Could not get latest info for %v: %v\n", pkgName, lvErr) + cliutils.ExitWithError() + } + + manifest = latestManifest + + // set p to a nil pointer with a concrete type so IsNil works correctly + if manifest.Scope.IsCluster() { + var p *v1alpha1.ClusterPackage + pkg = p + } else { + var p *v1alpha1.Package + pkg = p + } + } else { + // Unhandled error -> exit + fmt.Fprintf(os.Stderr, "āŒ Could not get resource: %v\n", pkgErr) + cliutils.ExitWithError() + } + } else { + if manifest, err = describe.GetManifestForPkg(ctx, pkg); err != nil { + fmt.Fprintf(os.Stderr, "āŒ Could not describe package %v: %v\n", pkgName, err) + cliutils.ExitWithError() + } + _, latestVersion, lvErr = describe.DescribeLatestVersion(ctx, + pkg.GetSpec().PackageInfo.RepositoryName, + pkg.GetSpec().PackageInfo.Name) + if lvErr != nil { + fmt.Fprintf(os.Stderr, "āŒ Could not get latest version: %v\n", err) + cliutils.ExitWithError() + } } - repos, err := repoClient.Meta().GetReposForPackage(pkgName) - if err != nil { - fmt.Fprintf(os.Stderr, "āŒ Could not get repos for %v: %v\n", pkgName, err) - cliutils.ExitWithError() + // if pkgName refers to a namespace-scoped manifest and not an installed package, show something about every instance + var pkgs []v1alpha1.Package + if pkg.IsNil() && manifest.Scope.IsNamespaced() { + client := cliutils.PackageClient(ctx) + var pkgList v1alpha1.PackageList + if err := client.Packages(describeCmdOptions.Namespace).GetAll(ctx, &pkgList); err != nil { + fmt.Fprintf(os.Stderr, "āŒ Could not list packages for %v: %v\n", pkgName, err) + cliutils.ExitWithError() + } + for _, pkg := range pkgList.Items { + if pkg.Spec.PackageInfo.Name == pkgName && + (describeCmdOptions.repository == "" || + pkg.Spec.PackageInfo.RepositoryName == describeCmdOptions.repository) { + + pkgs = append(pkgs, pkg) + } + } } - pkg, manifest, err := describe.DescribeInstalledPackage(ctx, pkgName) - if err != nil && !apierrors.IsNotFound(err) { - // Unhandled error -> exit - fmt.Fprintf(os.Stderr, "āŒ Could not describe package %v: %v\n", pkgName, err) + var repos []v1alpha1.PackageRepository + if pkg.IsNil() { + repos, err = repoClient.Meta().GetReposForPackage(pkgName) + } else { + repos, err = repoClient.Meta().GetReposForPackage(pkg.GetSpec().PackageInfo.Name) + } + if err != nil { + fmt.Fprintf(os.Stderr, "āŒ Could not get repos for %v: %v\n", pkgName, err) cliutils.ExitWithError() - } else if err != nil { - // package not installed -> use latest manifest from repo - manifest = latestManifest } bold := color.New(color.Bold).SprintFunc() if describeCmdOptions.Output == OutputFormatJSON { - printJSON(ctx, pkg, manifest, latestVersion, repos) + printJSON(ctx, pkg, pkgs, manifest, latestVersion, repos) } else if describeCmdOptions.Output == OutputFormatYAML { - printYAML(ctx, pkg, manifest, latestVersion, repos) + printYAML(ctx, pkg, pkgs, manifest, latestVersion, repos) } else { fmt.Println(bold("Package:"), nameAndDescription(manifest)) - fmt.Println(bold("Version:"), version(pkg, latestVersion)) - fmt.Println(bold("Status: "), status(pkg)) - if pkg != nil { + + if !pkg.IsNil() { + fmt.Println(bold("Version: "), version(pkg, latestVersion)) + fmt.Println(bold("Status: "), status(pkg)) fmt.Println(bold("Auto-Update:"), clientutils.AutoUpdateString(pkg, "Disabled")) + } else if len(pkgs) > 0 { + fmt.Println() + fmt.Println(bold("Instances:")) + for i, pkg := range pkgs { + fmt.Println(fmt.Sprintf(" %v.", i+1), bold("Name: "), pkg.Name) + fmt.Println(bold(" Namespace: "), pkg.Namespace) + fmt.Println(bold(" Version: "), version(&pkg, latestVersion)) + fmt.Println(bold(" Status: "), status(&pkg)) + fmt.Println(bold(" Auto-Update:"), clientutils.AutoUpdateString(&pkg, "Disabled")) + } } if len(manifest.Entrypoints) > 0 { @@ -108,10 +176,10 @@ var describeCmd = &cobra.Command{ printMarkdown(os.Stdout, trimmedDescription) } - if pkg != nil && len(pkg.Spec.Values) > 0 { + if !pkg.IsNil() && len(pkg.GetSpec().Values) > 0 { fmt.Println() fmt.Println(bold("Configuration:")) - printValueConfigurations(os.Stdout, pkg.Spec.Values) + printValueConfigurations(os.Stdout, pkg.GetSpec().Values) } } }, @@ -154,7 +222,7 @@ func printDependencies(manifest *v1alpha1.PackageManifest) { } } -func printRepositories(pkg *v1alpha1.ClusterPackage, repos []v1alpha1.PackageRepository) { +func printRepositories(pkg ctrlpkg.Package, repos []v1alpha1.PackageRepository) { for _, repo := range repos { fmt.Fprintf(os.Stderr, " * %v", repo.Name) if isInstalledFrom(pkg, repo) { @@ -165,7 +233,7 @@ func printRepositories(pkg *v1alpha1.ClusterPackage, repos []v1alpha1.PackageRep } } -func repositoriesAsMap(pkg *v1alpha1.ClusterPackage, repos []v1alpha1.PackageRepository) []map[string]any { +func repositoriesAsMap(pkg ctrlpkg.Package, repos []v1alpha1.PackageRepository) []map[string]any { repositories := make([]map[string]any, 0, len(repos)) for _, repo := range repos { repositories = append(repositories, map[string]any{ @@ -176,18 +244,18 @@ func repositoriesAsMap(pkg *v1alpha1.ClusterPackage, repos []v1alpha1.PackageRep return repositories } -func isInstalledFrom(pkg *v1alpha1.ClusterPackage, repo v1alpha1.PackageRepository) bool { - return pkg != nil && - (repo.Name == pkg.Spec.PackageInfo.RepositoryName || - (len(pkg.Spec.PackageInfo.RepositoryName) == 0 && repo.IsDefaultRepository())) +func isInstalledFrom(pkg ctrlpkg.Package, repo v1alpha1.PackageRepository) bool { + return !pkg.IsNil() && + (repo.Name == pkg.GetSpec().PackageInfo.RepositoryName || + (len(pkg.GetSpec().PackageInfo.RepositoryName) == 0 && repo.IsDefaultRepository())) } -func printReferences(ctx context.Context, pkg *v1alpha1.ClusterPackage, manifest *v1alpha1.PackageManifest) { +func printReferences(ctx context.Context, pkg ctrlpkg.Package, manifest *v1alpha1.PackageManifest) { repo := cliutils.RepositoryClientset(ctx) var repoClient repoclient.RepoClient - if pkg != nil { + if !pkg.IsNil() { repoClient = repo.ForPackage(pkg) - if url, err := repoClient.GetPackageManifestURL(manifest.Name, pkg.Spec.PackageInfo.Version); err != nil { + if url, err := repoClient.GetPackageManifestURL(manifest.Name, pkg.GetSpec().PackageInfo.Version); err != nil { fmt.Fprintf(os.Stderr, "āŒ Could not get package manifest url: %v\n", err) } else { fmt.Printf(" * Glasskube Package Manifest: %v\n", url) @@ -200,7 +268,7 @@ func printReferences(ctx context.Context, pkg *v1alpha1.ClusterPackage, manifest func referencesAsMap( ctx context.Context, - pkg *v1alpha1.ClusterPackage, + pkg ctrlpkg.Package, manifest *v1alpha1.PackageManifest, ) []map[string]string { references := []map[string]string{} @@ -210,10 +278,10 @@ func referencesAsMap( reference["url"] = ref.Url references = append(references, reference) } - if pkg != nil { + if !pkg.IsNil() { repo := cliutils.RepositoryClientset(ctx) repoClient := repo.ForPackage(pkg) - if url, err := repoClient.GetPackageManifestURL(manifest.Name, pkg.Spec.PackageInfo.Version); err == nil { + if url, err := repoClient.GetPackageManifestURL(manifest.Name, pkg.GetSpec().PackageInfo.Version); err == nil { reference := make(map[string]string) reference["label"] = "Glasskube Package Manifest" reference["url"] = url @@ -245,7 +313,7 @@ func printMarkdown(w io.Writer, text string) { } } -func status(pkg *v1alpha1.ClusterPackage) string { +func status(pkg ctrlpkg.Package) string { pkgStatus := client.GetStatusOrPending(pkg) if pkgStatus != nil { switch pkgStatus.Status { @@ -261,16 +329,18 @@ func status(pkg *v1alpha1.ClusterPackage) string { } } -func version(pkg *v1alpha1.ClusterPackage, latestVersion string) string { - if pkg != nil { +func version(pkg ctrlpkg.Package, latestVersion string) string { + if !pkg.IsNil() { var parts []string - if len(pkg.Status.Version) > 0 { - parts = append(parts, pkg.Status.Version) + if len(pkg.GetStatus().Version) > 0 { + parts = append(parts, pkg.GetStatus().Version) + } else { + parts = append(parts, "n/a") } - if pkg.Spec.PackageInfo.Version != pkg.Status.Version { - parts = append(parts, fmt.Sprintf("(desired: %v)", pkg.Spec.PackageInfo.Version)) + if pkg.GetSpec().PackageInfo.Version != pkg.GetStatus().Version { + parts = append(parts, fmt.Sprintf("(desired: %v)", pkg.GetSpec().PackageInfo.Version)) } - if semver.IsUpgradable(pkg.Spec.PackageInfo.Version, latestVersion) { + if semver.IsUpgradable(pkg.GetSpec().PackageInfo.Version, latestVersion) { parts = append(parts, fmt.Sprintf("(latest: %v)", latestVersion)) } return strings.Join(parts, " ") @@ -292,7 +362,8 @@ func nameAndDescription(manifest *v1alpha1.PackageManifest) string { func createOutputStructure( ctx context.Context, - pkg *v1alpha1.ClusterPackage, + pkg ctrlpkg.Package, + instances []v1alpha1.Package, manifest *v1alpha1.PackageManifest, latestVersion string, repos []v1alpha1.PackageRepository, @@ -308,23 +379,27 @@ func createOutputStructure( "repositories": repositoriesAsMap(pkg, repos), "references": referencesAsMap(ctx, pkg, manifest), } - if pkg != nil { - data["desiredVersion"] = pkg.Spec.PackageInfo.Version - data["configuration"] = pkg.Spec.Values - data["version"] = pkg.Status.Version + if !pkg.IsNil() { + data["desiredVersion"] = pkg.GetSpec().PackageInfo.Version + data["configuration"] = pkg.GetSpec().Values + data["version"] = pkg.GetStatus().Version data["autoUpdate"] = pkg.AutoUpdatesEnabled() - data["isUpgradable"] = semver.IsUpgradable(pkg.Spec.PackageInfo.Version, latestVersion) + data["isUpgradable"] = semver.IsUpgradable(pkg.GetSpec().PackageInfo.Version, latestVersion) data["status"] = client.GetStatusOrPending(pkg).Status } + if len(instances) > 0 { + data["instances"] = instances + } return data } func printJSON(ctx context.Context, - pkg *v1alpha1.ClusterPackage, + pkg ctrlpkg.Package, + pkgs []v1alpha1.Package, manifest *v1alpha1.PackageManifest, latestVersion string, repos []v1alpha1.PackageRepository) { - output := createOutputStructure(ctx, pkg, manifest, latestVersion, repos) + output := createOutputStructure(ctx, pkg, pkgs, manifest, latestVersion, repos) jsonOutput, err := json.MarshalIndent(output, "", " ") if err != nil { fmt.Fprintf(os.Stderr, "āŒ Could not marshal JSON output: %v\n", err) @@ -334,11 +409,12 @@ func printJSON(ctx context.Context, } func printYAML(ctx context.Context, - pkg *v1alpha1.ClusterPackage, + pkg ctrlpkg.Package, + pkgs []v1alpha1.Package, manifest *v1alpha1.PackageManifest, latestVersion string, repos []v1alpha1.PackageRepository) { - output := createOutputStructure(ctx, pkg, manifest, latestVersion, repos) + output := createOutputStructure(ctx, pkg, pkgs, manifest, latestVersion, repos) yamlOutput, err := yaml.Marshal(output) if err != nil { fmt.Fprintf(os.Stderr, "āŒ Could not marshal YAML output: %v\n", err) @@ -350,6 +426,8 @@ func printYAML(ctx context.Context, func init() { describeCmd.Flags().StringVar(&describeCmdOptions.repository, "repository", describeCmdOptions.repository, "specify the name of the package repository used to use when the package is not installed") - RootCmd.AddCommand(describeCmd) describeCmdOptions.OutputOptions.AddFlagsToCommand(describeCmd) + describeCmdOptions.KindOptions.AddFlagsToCommand(describeCmd) + describeCmdOptions.NamespaceOptions.AddFlagsToCommand(describeCmd) + RootCmd.AddCommand(describeCmd) } diff --git a/cmd/glasskube/cmd/install.go b/cmd/glasskube/cmd/install.go index eb9403d6f..e62d3a315 100644 --- a/cmd/glasskube/cmd/install.go +++ b/cmd/glasskube/cmd/install.go @@ -11,6 +11,7 @@ import ( "github.com/glasskube/glasskube/internal/clicontext" "github.com/glasskube/glasskube/internal/cliutils" "github.com/glasskube/glasskube/internal/config" + "github.com/glasskube/glasskube/internal/controller/ctrlpkg" "github.com/glasskube/glasskube/internal/dependency" "github.com/glasskube/glasskube/internal/manifestvalues/cli" "github.com/glasskube/glasskube/internal/manifestvalues/flags" @@ -38,16 +39,16 @@ var installCmdOptions = struct { Yes bool DryRun bool OutputOptions + NamespaceOptions }{ ValuesOptions: flags.NewOptions(), - OutputOptions: OutputOptions{}, } var installCmd = &cobra.Command{ - Use: "install [package-name]", + Use: "install []", Short: "Install a package", Long: `Install a package.`, - Args: cobra.ExactArgs(1), + Args: cobra.RangeArgs(1, 2), PreRun: cliutils.SetupClientContext(true, &rootCmdOptions.SkipUpdateCheck), ValidArgsFunction: completeAvailablePackageNames, Run: func(cmd *cobra.Command, args []string) { @@ -65,7 +66,7 @@ var installCmd = &cobra.Command{ bold := color.New(color.Bold).SprintFunc() packageName := args[0] - pkgBuilder := client.ClusterPackageBuilder(packageName) + pkgBuilder := client.PackageBuilder(packageName) var repoClient repoclient.RepoClient if len(installCmdOptions.Repository) > 0 { @@ -118,15 +119,48 @@ var installCmd = &cobra.Command{ pkgBuilder.WithVersion(installCmdOptions.Version) - installationPlan := []dependency.Requirement{ - {PackageWithVersion: dependency.PackageWithVersion{Name: packageName, Version: installCmdOptions.Version}}, - } - var manifest v1alpha1.PackageManifest if err := repoClient.FetchPackageManifest(packageName, installCmdOptions.Version, &manifest); err != nil { fmt.Fprintf(os.Stderr, "ā— Error: Could not fetch package manifest: %v\n", err) cliutils.ExitWithError() - } else if validationResult, err := + } + + installationPlan := []dependency.Requirement{} + if manifest.Scope.IsCluster() { + if len(args) != 1 { + fmt.Fprintf(os.Stderr, + "āŒ %v has scope Cluster. Specifying an instance name for a ClusterPackage is not possible\n", + packageName) + cliutils.ExitWithError() + } + installationPlan = append(installationPlan, + dependency.Requirement{PackageWithVersion: dependency.PackageWithVersion{ + Name: packageName, + Version: installCmdOptions.Version, + }}, + ) + } else { + var name string + if len(args) != 2 { + fmt.Fprintf(os.Stderr, "%v has scope Namespaced. Please enter a name (default %v):\n", packageName, packageName) + name = cliutils.GetInputStr("name") + if name == "" { + name = packageName + } + } else { + name = args[1] + } + ns := installCmdOptions.GetActualNamespace(ctx) + pkgBuilder.WithName(name).WithNamespace(ns) + installationPlan = append(installationPlan, + dependency.Requirement{PackageWithVersion: dependency.PackageWithVersion{ + Name: fmt.Sprintf("%v of type %v in namespace %v", name, packageName, ns), + Version: installCmdOptions.Version, + }}, + ) + } + + if validationResult, err := dm.Validate(ctx, &manifest, installCmdOptions.Version); err != nil { fmt.Fprintf(os.Stderr, "ā— Error: Could not validate dependencies: %v\n", err) cliutils.ExitWithError() @@ -159,7 +193,7 @@ var installCmd = &cobra.Command{ pkgBuilder.WithAutoUpdates(installCmdOptions.EnableAutoUpdates) - pkg := pkgBuilder.Build() + pkg := pkgBuilder.Build(manifest.Scope) fmt.Fprintln(os.Stderr, bold("Summary:")) fmt.Fprintf(os.Stderr, " * The following packages will be installed in your cluster (%v):\n", config.CurrentContext) @@ -172,10 +206,10 @@ var installCmd = &cobra.Command{ fmt.Fprintln(os.Stderr, " * Automatic updates will be", bold("not enabled")) } - if len(pkg.Spec.Values) > 0 { + if len(pkg.GetSpec().Values) > 0 { fmt.Fprintln(os.Stderr, bold("Configuration:")) - printValueConfigurations(os.Stderr, pkg.Spec.Values) - if _, err := valueResolver.Resolve(ctx, pkg.Spec.Values); err != nil { + printValueConfigurations(os.Stderr, pkg.GetSpec().Values) + if _, err := valueResolver.Resolve(ctx, pkg.GetSpec().Values); err != nil { fmt.Fprintf(os.Stderr, "āš ļø Some values can not be resolved: %v\n", err) } } @@ -228,7 +262,7 @@ var installCmd = &cobra.Command{ }, } -func formatOutput(pkg *v1alpha1.ClusterPackage, format OutputFormat) (string, error) { +func formatOutput(pkg ctrlpkg.Package, format OutputFormat) (string, error) { if gvks, _, err := scheme.Scheme.ObjectKinds(pkg); err == nil && len(gvks) == 1 { pkg.SetGroupVersionKind(gvks[0]) } @@ -328,5 +362,6 @@ func init() { installCmd.MarkFlagsMutuallyExclusive("version", "enable-auto-updates") installCmdOptions.ValuesOptions.AddFlagsToCommand(installCmd) installCmdOptions.OutputOptions.AddFlagsToCommand(installCmd) + installCmdOptions.NamespaceOptions.AddFlagsToCommand(installCmd) RootCmd.AddCommand(installCmd) } diff --git a/cmd/glasskube/cmd/kind_options.go b/cmd/glasskube/cmd/kind_options.go index ebaeb636b..6b66671df 100644 --- a/cmd/glasskube/cmd/kind_options.go +++ b/cmd/glasskube/cmd/kind_options.go @@ -39,7 +39,7 @@ func (kind *ResourceKind) String() string { // Type implements pflag.Value. func (r *ResourceKind) Type() string { - return fmt.Sprintf("[%v|%v]", KindPackage, KindClusterPackage) + return fmt.Sprintf("(%v|%v)", KindPackage, KindClusterPackage) } type KindOptions struct { @@ -52,8 +52,7 @@ func (opts *KindOptions) AddFlagsToCommand(cmd *cobra.Command) { func DefaultKindOptions() KindOptions { return KindOptions{ - // TODO: Change to KindUnspecified to support namespaced packages - Kind: KindClusterPackage, + Kind: KindUnspecified, } } @@ -105,7 +104,7 @@ func getPackageOrClusterPackage( pNotFound := !pkgTried || errp != nil cpNotFound := !cpkgTried || errcp != nil if pNotFound && cpNotFound { - return nil, fmt.Errorf("no Package or ClusterPackage found with name %v", name) + return nil, fmt.Errorf("no Package or ClusterPackage found with name %v: %w; %w", name, errp, errcp) } else if !pNotFound && !cpNotFound { return nil, fmt.Errorf("both Package and ClusterPackage found with name %v. Please specify the kind explicitly", name) } else if !pNotFound && cpNotFound { diff --git a/cmd/glasskube/cmd/list.go b/cmd/glasskube/cmd/list.go index a87ff38a6..7d5cf4d8f 100644 --- a/cmd/glasskube/cmd/list.go +++ b/cmd/glasskube/cmd/list.go @@ -6,6 +6,8 @@ import ( "os" "strings" + "github.com/glasskube/glasskube/internal/controller/ctrlpkg" + "github.com/glasskube/glasskube/internal/clientutils" "github.com/glasskube/glasskube/internal/cliutils" "github.com/glasskube/glasskube/internal/semver" @@ -21,6 +23,7 @@ type ListCmdOptions struct { ShowLatestVersion bool More bool OutputOptions + KindOptions } func (o ListCmdOptions) toListOptions() list.ListOptions { @@ -30,7 +33,9 @@ func (o ListCmdOptions) toListOptions() list.ListOptions { } } -var listCmdOptions = ListCmdOptions{} +var listCmdOptions = ListCmdOptions{ + KindOptions: DefaultKindOptions(), +} var listCmd = &cobra.Command{ Use: "list", @@ -45,49 +50,55 @@ var listCmd = &cobra.Command{ listCmdOptions.ShowLatestVersion = true listCmdOptions.ShowDescription = true } - pkgs, err := list.NewLister(ctx).GetPackagesWithStatus(ctx, listCmdOptions.toListOptions()) - if err != nil { - fmt.Fprintf(os.Stderr, "ā— An error occurred listing packages: %v\n", err) - if len(pkgs) == 0 { - cliutils.ExitWithError() - } else { - fmt.Fprint(os.Stderr, "āš ļø The table shown below may be incomplete due to the error above.\n\n") - } + lister := list.NewListerWithRepoCache(ctx) + var clPkgs []*list.PackageWithStatus + var pkgs []*list.PackagesWithStatus + var err error + if listCmdOptions.Kind != KindPackage { + clPkgs, err = lister.GetClusterPackagesWithStatus(ctx, listCmdOptions.toListOptions()) + handleListErr(len(clPkgs), err, "clusterpackages") } - if len(pkgs) == 0 { - if listCmdOptions.ListOutdatedOnly { - fmt.Fprintln(os.Stderr, "All installed packages are up-to-date.") - } else if listCmdOptions.ListInstalledOnly { - fmt.Fprintln(os.Stderr, "There are currently no packages installed in your cluster.\n"+ - "Run \"glasskube help install\" to get started.") - } else { - fmt.Fprintln(os.Stderr, "No packages found. This is probably a bug.") + if listCmdOptions.Kind != KindClusterPackage { + pkgs, err = lister.GetPackagesWithStatus(ctx, listCmdOptions.toListOptions()) + handleListErr(len(pkgs), err, "packages") + } + noPkgs := len(pkgs) == 0 && listCmdOptions.Kind != KindClusterPackage + noClPkgs := len(clPkgs) == 0 && listCmdOptions.Kind != KindPackage + if noPkgs { + handleEmptyList("packages") + } else if len(pkgs) > 0 { + printPackageTable(pkgs) + if listCmdOptions.Kind != KindPackage { + fmt.Fprintln(os.Stderr, "") } - } else { + } + if noClPkgs { + handleEmptyList("clusterpackages") + } else if len(clPkgs) > 0 { if listCmdOptions.Output == OutputFormatJSON { - printPackageJSON(pkgs) + printPackageJSON(clPkgs) } else if listCmdOptions.Output == OutputFormatYAML { - printPackageYAML(pkgs) + printPackageYAML(clPkgs) } else { - printPackageTable(pkgs) + printClusterPackageTable(clPkgs) } } - }, } func init() { listCmd.PersistentFlags().BoolVarP(&listCmdOptions.ListInstalledOnly, "installed", "i", false, - "list only installed packages") + "list only installed (cluster-)packages") listCmd.PersistentFlags().BoolVar(&listCmdOptions.ListOutdatedOnly, "outdated", false, - "list only outdated packages") + "list only outdated (cluster-)packages") listCmd.PersistentFlags().BoolVar(&listCmdOptions.ShowDescription, "show-description", false, - "show the package description") + "show the (cluster-)package description") listCmd.PersistentFlags().BoolVar(&listCmdOptions.ShowLatestVersion, "show-latest", false, - "show the latest version of packages if available") + "show the latest version of (cluster-)packages if available") listCmd.PersistentFlags().BoolVarP(&listCmdOptions.More, "more", "m", false, - "show additional information about packages (like --show-description --show-latest)") + "show additional information about (cluster-)packages (like --show-description --show-latest)") listCmdOptions.OutputOptions.AddFlagsToCommand(listCmd) + listCmdOptions.KindOptions.AddFlagsToCommand(listCmd) listCmd.MarkFlagsMutuallyExclusive("show-description", "more") listCmd.MarkFlagsMutuallyExclusive("show-latest", "more") @@ -95,7 +106,29 @@ func init() { RootCmd.AddCommand(listCmd) } -func printPackageTable(packages []*list.PackageWithStatus) { +func handleListErr(listLen int, err error, resource string) { + if err != nil { + fmt.Fprintf(os.Stderr, "ā— An error occurred listing %s: %v\n", resource, err) + if listLen == 0 { + cliutils.ExitWithError() + } else { + fmt.Fprint(os.Stderr, "āš ļø The table shown below may be incomplete due to the error above.\n\n") + } + } +} + +func handleEmptyList(resource string) { + if listCmdOptions.ListOutdatedOnly { + fmt.Fprintf(os.Stderr, "All installed %s are up-to-date.\n", resource) + } else if listCmdOptions.ListInstalledOnly { + fmt.Fprintf(os.Stderr, "There are currently no %s installed in your cluster.\n"+ + "Run \"glasskube help install\" to get started.\n", resource) + } else { + fmt.Fprintf(os.Stderr, "No %s found in the available repositories.\n", resource) + } +} + +func printClusterPackageTable(packages []*list.PackageWithStatus) { header := []string{"NAME", "STATUS", "VERSION", "AUTO-UPDATE"} if listCmdOptions.ShowLatestVersion { header = append(header, "LATEST VERSION") @@ -104,11 +137,67 @@ func printPackageTable(packages []*list.PackageWithStatus) { if listCmdOptions.ShowDescription { header = append(header, "DESCRIPTION") } + err := cliutils.PrintTable(os.Stdout, packages, header, func(pkg *list.PackageWithStatus) []string { - row := []string{pkg.Name, statusString(*pkg), versionString(*pkg), clientutils.AutoUpdateString(pkg.Package, "")} + row := []string{pkg.Name, statusString(*pkg), versionString(*pkg), + clientutils.AutoUpdateString(pkg.ClusterPackage, "")} + if listCmdOptions.ShowLatestVersion { + row = append(row, pkg.LatestVersion) + } + s := make([]string, len(pkg.Repos)) + if pkg.ClusterPackage != nil { + for i, r := range pkg.Repos { + if pkg.ClusterPackage.Spec.PackageInfo.RepositoryName == r { + s[i] = fmt.Sprintf("%v (used)", r) + } else { + s[i] = r + } + } + } else { + s = pkg.Repos + } + row = append(row, strings.Join(s, ", ")) + if listCmdOptions.ShowDescription { + row = append(row, pkg.ShortDescription) + } + return row + }) + if err != nil { + fmt.Fprintf(os.Stderr, "There was an error displaying the clusterpackage table:\n%v\n(This is a bug)\n", err) + cliutils.ExitWithError() + } +} + +func printPackageTable(packages []*list.PackagesWithStatus) { + header := []string{"PACKAGENAME", "NAMESPACE", "NAME", "STATUS", "VERSION", "AUTO-UPDATE"} + if listCmdOptions.ShowLatestVersion { + header = append(header, "LATEST VERSION") + } + header = append(header, "REPOSITORY") + if listCmdOptions.ShowDescription { + header = append(header, "DESCRIPTION") + } + + var flattenedPkgs []*list.PackageWithStatus + for _, pkgs := range packages { + if len(pkgs.Packages) == 0 { + flattenedPkgs = append(flattenedPkgs, &list.PackageWithStatus{ + MetaIndexItem: pkgs.MetaIndexItem, + }) + } else { + flattenedPkgs = append(flattenedPkgs, pkgs.Packages...) + } + } + + err := cliutils.PrintTable(os.Stdout, + flattenedPkgs, + header, + func(pkg *list.PackageWithStatus) []string { + row := []string{pkg.Name, pkgNamespaceString(*pkg), pkgNameString(*pkg), statusString(*pkg), versionString(*pkg), + clientutils.AutoUpdateString(pkg.Package, "")} if listCmdOptions.ShowLatestVersion { row = append(row, pkg.LatestVersion) } @@ -147,7 +236,6 @@ func printPackageJSON(packages []*list.PackageWithStatus) { } func printPackageYAML(packages []*list.PackageWithStatus) { - for i, pkg := range packages { yamlData, err := yaml.Marshal(pkg) if err != nil { @@ -163,6 +251,22 @@ func printPackageYAML(packages []*list.PackageWithStatus) { } } +func pkgNamespaceString(pkg list.PackageWithStatus) string { + if pkg.Package != nil { + return pkg.Package.Namespace + } else { + return "" + } +} + +func pkgNameString(pkg list.PackageWithStatus) string { + if pkg.Package != nil { + return pkg.Package.Name + } else { + return "" + } +} + func statusString(pkg list.PackageWithStatus) string { if pkg.Status != nil { return pkg.Status.Status @@ -172,9 +276,15 @@ func statusString(pkg list.PackageWithStatus) string { } func versionString(pkg list.PackageWithStatus) string { - if pkg.Package != nil { - specVersion := pkg.Package.Spec.PackageInfo.Version - statusVersion := pkg.Package.Status.Version + var p ctrlpkg.Package + if pkg.ClusterPackage != nil { + p = pkg.ClusterPackage + } else if pkg.Package != nil { + p = pkg.Package + } + if pkg.ClusterPackage != nil || pkg.Package != nil { + specVersion := p.GetSpec().PackageInfo.Version + statusVersion := p.GetStatus().Version repoVersion := pkg.LatestVersion if statusVersion != "" { diff --git a/cmd/glasskube/cmd/namespace_options.go b/cmd/glasskube/cmd/namespace_options.go index 2ebec1b8c..aaae9c049 100644 --- a/cmd/glasskube/cmd/namespace_options.go +++ b/cmd/glasskube/cmd/namespace_options.go @@ -16,8 +16,8 @@ func (opt *NamespaceOptions) AddFlagsToCommand(cmd *cobra.Command) { } func (opt *NamespaceOptions) GetActualNamespace(ctx context.Context) string { - if configureCmdOptions.Namespace != "" { - return configureCmdOptions.Namespace + if opt.Namespace != "" { + return opt.Namespace } else { rawConfig := clicontext.RawConfigFromContext(ctx) if current, ok := rawConfig.Contexts[rawConfig.CurrentContext]; ok && current.Namespace != "" { diff --git a/cmd/glasskube/cmd/open.go b/cmd/glasskube/cmd/open.go index 36f2d9693..3835e80b4 100644 --- a/cmd/glasskube/cmd/open.go +++ b/cmd/glasskube/cmd/open.go @@ -24,7 +24,7 @@ var ( ) var openCmd = &cobra.Command{ - Use: "open [package-name] [entrypoint]", + Use: "open []", Short: "Open the Web UI of a package", Long: `Open the Web UI of a package. If the package manifest has more than one entrypoint, specify the name of the entrypoint to open.`, @@ -89,9 +89,8 @@ If the package manifest has more than one entrypoint, specify the name of the en } func init() { - // TODO: Enable these flags to support namespaced packages - // openCmdOptions.KindOptions.AddFlagsToCommand(openCmd) - // openCmdOptions.NamespaceOptions.AddFlagsToCommand(openCmd) + openCmdOptions.KindOptions.AddFlagsToCommand(openCmd) + openCmdOptions.NamespaceOptions.AddFlagsToCommand(openCmd) openCmd.Flags().Int32Var(&openCmdOptions.Port, "port", openCmdOptions.Port, "custom port for opening the package") RootCmd.AddCommand(openCmd) } diff --git a/cmd/glasskube/cmd/output_options.go b/cmd/glasskube/cmd/output_options.go index 420055114..cfafa7e40 100644 --- a/cmd/glasskube/cmd/output_options.go +++ b/cmd/glasskube/cmd/output_options.go @@ -28,7 +28,7 @@ func (of *OutputFormat) Set(value string) error { } func (of *OutputFormat) Type() string { - return fmt.Sprintf("[%v|%v]", OutputFormatJSON, OutputFormatYAML) + return fmt.Sprintf("(%v|%v)", OutputFormatJSON, OutputFormatYAML) } type OutputOptions struct { diff --git a/cmd/glasskube/cmd/repo_add.go b/cmd/glasskube/cmd/repo_add.go index f86a996d9..a662ea3e3 100644 --- a/cmd/glasskube/cmd/repo_add.go +++ b/cmd/glasskube/cmd/repo_add.go @@ -14,7 +14,7 @@ import ( var repoAddCmdOptions = repoOptions{} var repoAddCmd = &cobra.Command{ - Use: "add [name] [url]", + Use: "add ", Short: "Add a package repository to the current cluster", Args: cobra.ExactArgs(2), PreRun: cliutils.SetupClientContext(true, &rootCmdOptions.SkipUpdateCheck), diff --git a/cmd/glasskube/cmd/repo_delete.go b/cmd/glasskube/cmd/repo_delete.go new file mode 100644 index 000000000..8890dac1d --- /dev/null +++ b/cmd/glasskube/cmd/repo_delete.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/glasskube/glasskube/api/v1alpha1" + "github.com/glasskube/glasskube/internal/cliutils" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var repoDeleteCmd = &cobra.Command{ + Use: "delete [repositoryName]", + Short: "Delete a repository", + Args: cobra.ExactArgs(1), + PreRun: cliutils.SetupClientContext(true, &rootCmdOptions.SkipUpdateCheck), + Run: func(cmd *cobra.Command, args []string) { + repositoryName := args[0] + ctx := cmd.Context() + deleteRepository(ctx, repositoryName) + }, +} + +func deleteRepository(ctx context.Context, repoName string) { + client := cliutils.PackageClient(ctx) + + var targetRepo v1alpha1.PackageRepository + if err := client.PackageRepositories().Get(ctx, repoName, &targetRepo); err != nil { + fmt.Fprintf(os.Stderr, "āŒ error listing package repository: %v\n", err) + cliutils.ExitWithError() + } + + var pkgs v1alpha1.PackageList + if err := client.Packages("").GetAll(ctx, &pkgs); err != nil { + fmt.Fprintf(os.Stderr, "Could not list packages: %v", err) + cliutils.ExitWithError() + } + + var clpkgs v1alpha1.ClusterPackageList + if err := client.ClusterPackages().GetAll(ctx, &clpkgs); err != nil { + fmt.Fprintf(os.Stderr, "Could not list Cluster packages: %v", err) + cliutils.ExitWithError() + } + + repoPackages := getPackagesFromRepo(clpkgs, pkgs, repoName) + if len(repoPackages) > 0 { + fmt.Printf("Repository %s cannot be deleted, because the following packages are installed from this repository: %v\n", + repoName, strings.Join(repoPackages, ", ")) + cliutils.ExitWithError() + } + + if !cliutils.YesNoPrompt(fmt.Sprintf("Repository %s will now be deleted. Do you want to continue?", repoName), false) { + fmt.Println("āŒ Repository Deletion Cancelled") + cliutils.ExitWithError() + } + err := client.PackageRepositories().Delete(ctx, &targetRepo, metav1.DeleteOptions{}) + if err != nil { + fmt.Println("Error deleting repository:", err) + cliutils.ExitWithError() + } + + fmt.Printf("Repository %s has been deleted.\n", repoName) +} + +func getPackagesFromRepo(clpkgs v1alpha1.ClusterPackageList, pkgs v1alpha1.PackageList, repoName string) []string { + var repoPackages []string + for _, clpkg := range clpkgs.Items { + if clpkg.Spec.PackageInfo.RepositoryName == repoName { + repoPackages = append(repoPackages, clpkg.Name) + } + } + + for _, pkg := range pkgs.Items { + if pkg.Spec.PackageInfo.RepositoryName == repoName { + repoPackages = append(repoPackages, pkg.Name) + } + } + return repoPackages +} + +func init() { + repoCmd.AddCommand(repoDeleteCmd) +} diff --git a/cmd/glasskube/cmd/repo_options.go b/cmd/glasskube/cmd/repo_options.go index ad02ebe74..b0ffdc5b3 100644 --- a/cmd/glasskube/cmd/repo_options.go +++ b/cmd/glasskube/cmd/repo_options.go @@ -28,7 +28,7 @@ func (t *repoAuthType) Set(v string) error { } func (e *repoAuthType) Type() string { - return fmt.Sprintf("[%v|%v|%v]", repoNoAuth, repoBasicAuth, repoBearerAuth) + return fmt.Sprintf("(%v|%v|%v)", repoNoAuth, repoBasicAuth, repoBearerAuth) } const ( diff --git a/cmd/glasskube/cmd/repo_update.go b/cmd/glasskube/cmd/repo_update.go index a1b685f31..3836267dc 100644 --- a/cmd/glasskube/cmd/repo_update.go +++ b/cmd/glasskube/cmd/repo_update.go @@ -13,7 +13,7 @@ import ( var repoUpdateCmdOptions = repoOptions{} var repoUpdateCmd = &cobra.Command{ - Use: "update [name]", + Use: "update ", Short: "Update a package repository for the current cluster", Args: cobra.ExactArgs(1), PreRun: cliutils.SetupClientContext(true, &rootCmdOptions.SkipUpdateCheck), diff --git a/cmd/glasskube/cmd/telemetry.go b/cmd/glasskube/cmd/telemetry.go index 0ac98d374..ea713d811 100644 --- a/cmd/glasskube/cmd/telemetry.go +++ b/cmd/glasskube/cmd/telemetry.go @@ -39,7 +39,7 @@ var telemetryStatusCmd = &cobra.Command{ } var telemetryCmd = &cobra.Command{ - Use: "telemetry ", + Use: "telemetry (enable|disable)", Short: "View and modify telemetry settings", Long: "View and modify telemetry settings. \n" + "For more information on how Glasskube uses telemetry see https://glasskube.dev/telemetry", diff --git a/cmd/glasskube/cmd/uninstall.go b/cmd/glasskube/cmd/uninstall.go index 7fd9d0cb8..6eaaf850e 100644 --- a/cmd/glasskube/cmd/uninstall.go +++ b/cmd/glasskube/cmd/uninstall.go @@ -5,22 +5,24 @@ import ( "os" "github.com/fatih/color" - "github.com/glasskube/glasskube/api/v1alpha1" "github.com/glasskube/glasskube/internal/clicontext" "github.com/glasskube/glasskube/internal/cliutils" "github.com/glasskube/glasskube/pkg/statuswriter" "github.com/glasskube/glasskube/pkg/uninstall" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var uninstallCmdOptions = struct { NoWait bool Yes bool -}{} + KindOptions + NamespaceOptions +}{ + KindOptions: DefaultKindOptions(), +} var uninstallCmd = &cobra.Command{ - Use: "uninstall [package-name]", + Use: "uninstall ", Short: "Uninstall a package", Long: `Uninstall a package.`, Args: cobra.ExactArgs(1), @@ -32,39 +34,46 @@ var uninstallCmd = &cobra.Command{ currentContext := clicontext.RawConfigFromContext(ctx).CurrentContext client := clicontext.PackageClientFromContext(ctx) dm := cliutils.DependencyManager(ctx) + uninstaller := uninstall.NewUninstaller(client) + if !rootCmdOptions.NoProgress { + uninstaller.WithStatusWriter(statuswriter.Spinner()) + } - if g, err := dm.NewGraph(ctx); err != nil { - fmt.Fprintf(os.Stderr, "āŒ Error validating uninstall: %v\n", err) + pkg, err := getPackageOrClusterPackage( + ctx, pkgName, uninstallCmdOptions.KindOptions, uninstallCmdOptions.NamespaceOptions) + if err != nil { + fmt.Fprintf(os.Stderr, "āŒ Could not get resource: %v\n", err) cliutils.ExitWithError() - } else { - g.Delete(pkgName) - pruned := g.Prune() - if err := g.Validate(); err != nil { - fmt.Fprintf(os.Stderr, "āŒ %v can not be uninstalled for the following reason: %v\n", pkgName, err) + } + + if !pkg.IsNamespaceScoped() { + if g, err := dm.NewGraph(ctx); err != nil { + fmt.Fprintf(os.Stderr, "āŒ Error validating uninstall: %v\n", err) cliutils.ExitWithError() } else { - showUninstallDetails(currentContext, pkgName, pruned) - if !uninstallCmdOptions.Yes && !cliutils.YesNoPrompt("Do you want to continue?", false) { - fmt.Println("āŒ Uninstallation cancelled.") - cliutils.ExitSuccess() + g.Delete(pkgName) + pruned := g.Prune() + if err := g.Validate(); err != nil { + fmt.Fprintf(os.Stderr, "āŒ %v can not be uninstalled for the following reason: %v\n", pkgName, err) + cliutils.ExitWithError() + } else { + showUninstallDetails(currentContext, pkgName, pruned) + if !uninstallCmdOptions.Yes && !cliutils.YesNoPrompt("Do you want to continue?", false) { + fmt.Println("āŒ Uninstallation cancelled.") + cliutils.ExitSuccess() + } } } } - uninstaller := uninstall.NewUninstaller(client) - if !rootCmdOptions.NoProgress { - uninstaller.WithStatusWriter(statuswriter.Spinner()) - } - - pkg := v1alpha1.ClusterPackage{ObjectMeta: metav1.ObjectMeta{Name: pkgName}} if uninstallCmdOptions.NoWait { - if err := uninstaller.Uninstall(ctx, &pkg); err != nil { + if err := uninstaller.Uninstall(ctx, pkg); err != nil { fmt.Fprintf(os.Stderr, "\nāŒ An error occurred during uninstallation:\n\n%v\n", err) cliutils.ExitWithError() } fmt.Fprintln(os.Stderr, "Uninstallation started in background") } else { - if err := uninstaller.UninstallBlocking(ctx, &pkg); err != nil { + if err := uninstaller.UninstallBlocking(ctx, pkg); err != nil { fmt.Fprintf(os.Stderr, "\nāŒ An error occurred during uninstallation:\n\n%v\n", err) cliutils.ExitWithError() } @@ -85,6 +94,8 @@ func showUninstallDetails(context, name string, pruned []string) { } func init() { + uninstallCmdOptions.KindOptions.AddFlagsToCommand(uninstallCmd) + uninstallCmdOptions.NamespaceOptions.AddFlagsToCommand(uninstallCmd) uninstallCmd.PersistentFlags().BoolVar(&uninstallCmdOptions.NoWait, "no-wait", false, "perform non-blocking uninstall") uninstallCmd.PersistentFlags().BoolVarP(&uninstallCmdOptions.Yes, "yes", "y", false, diff --git a/cmd/glasskube/cmd/update.go b/cmd/glasskube/cmd/update.go index 3c9029968..3e0fd7413 100644 --- a/cmd/glasskube/cmd/update.go +++ b/cmd/glasskube/cmd/update.go @@ -9,6 +9,8 @@ import ( "strings" "text/tabwriter" + "github.com/glasskube/glasskube/internal/controller/ctrlpkg" + "github.com/glasskube/glasskube/api/v1alpha1" "github.com/glasskube/glasskube/internal/clicontext" "github.com/glasskube/glasskube/internal/cliutils" @@ -21,6 +23,7 @@ import ( "github.com/glasskube/glasskube/pkg/update" "github.com/spf13/cobra" "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/cache" "sigs.k8s.io/yaml" ) @@ -28,16 +31,17 @@ var updateCmdOptions struct { Version string Yes bool OutputOptions + NamespaceOptions + KindOptions } var updateCmd = &cobra.Command{ - Use: "update [packages...]", + Use: "update [...]", Short: "Update some or all packages in your cluster", PreRun: cliutils.SetupClientContext(true, &rootCmdOptions.SkipUpdateCheck), ValidArgsFunction: completeInstalledPackageNames, Run: func(cmd *cobra.Command, args []string) { ctx := cmd.Context() - packageNames := args updater := update.NewUpdater(ctx) if !rootCmdOptions.NoProgress { @@ -51,17 +55,51 @@ var updateCmd = &cobra.Command{ fmt.Fprintf(os.Stderr, "Updating to specific version is only possible for a single package\n") cliutils.ExitWithError() } + if len(args) == 1 && updateCmdOptions.Version != "" { if !strings.HasPrefix(updateCmdOptions.Version, "v") { updateCmdOptions.Version = "v" + updateCmdOptions.Version } - tx, err = updater.PrepareForVersion(ctx, args[0], updateCmdOptions.Version) - if err != nil { - fmt.Fprintf(os.Stderr, "error in updating the package version : %v\n", err) + + if pkg, err := getPackageOrClusterPackage(ctx, args[0], + updateCmdOptions.KindOptions, updateCmdOptions.NamespaceOptions); err != nil { + fmt.Fprintf(os.Stderr, "Could not get %v: %v\n", args[0], err) cliutils.ExitWithError() + } else { + tx, err = updater.PrepareForVersion(ctx, pkg, updateCmdOptions.Version) + if err != nil { + fmt.Fprintf(os.Stderr, "error in updating the package version : %v\n", err) + cliutils.ExitWithError() + } } } else { - tx, err = updater.Prepare(ctx, packageNames) + var updateGetters []update.PackagesGetter + if len(args) > 0 { + pkgs := make([]ctrlpkg.Package, len(args)) + for i, name := range args { + if pkg, err := getPackageOrClusterPackage(ctx, name, + updateCmdOptions.KindOptions, updateCmdOptions.NamespaceOptions); err != nil { + fmt.Fprintf(os.Stderr, "Could not get %v: %v\n", name, err) + cliutils.ExitWithError() + } else { + pkgs[i] = pkg + } + } + updateGetters = append(updateGetters, update.GetExact(pkgs)) + } else if updateCmdOptions.Namespace != "" { + updateGetters = append(updateGetters, update.GetAllPackages(updateCmdOptions.Namespace)) + } else { + switch updateCmdOptions.Kind { + case KindClusterPackage: + updateGetters = append(updateGetters, update.GetAllClusterPackages()) + case KindPackage: + updateGetters = append(updateGetters, update.GetAllPackages("")) + default: + updateGetters = append(updateGetters, update.GetAllClusterPackages(), update.GetAllPackages("")) + } + } + + tx, err = updater.Prepare(ctx, updateGetters...) if err != nil { fmt.Fprintf(os.Stderr, "āŒ update preparation failed: %v\n", err) cliutils.ExitWithError() @@ -74,7 +112,7 @@ var updateCmd = &cobra.Command{ fmt.Fprintf(os.Stderr, "ā›” Update cancelled. No changes were made.\n") cliutils.ExitSuccess() } - updatedPackages, err := updater.Apply(ctx, tx) + updatedPackages, err := updater.ApplyBlocking(ctx, tx) if err != nil { fmt.Fprintf(os.Stderr, "āŒ update failed: %v\n", err) cliutils.ExitWithError() @@ -90,11 +128,18 @@ func printTransaction(tx update.UpdateTransaction) { w := tabwriter.NewWriter(os.Stderr, 0, 0, 1, ' ', 0) for _, item := range tx.Items { if item.UpdateRequired() { - fmt.Fprintf(w, "%v:\t%v\t-> %v\n", - item.Package.Name, item.Package.Spec.PackageInfo.Version, item.Version) + fmt.Fprintf(w, "%v\t%v:\t%v\t-> %v\n", + item.Package.GetSpec().PackageInfo.Name, + cache.MetaObjectToName(item.Package), + item.Package.GetSpec().PackageInfo.Version, + item.Version, + ) } else { - fmt.Fprintf(w, "%v:\t%v\t(up-to-date)\n", - item.Package.Name, item.Package.Spec.PackageInfo.Version) + fmt.Fprintf(w, "%v\t%v:\t%v\t(up-to-date)\n", + item.Package.GetSpec().PackageInfo.Name, + cache.MetaObjectToName(item.Package), + item.Package.GetSpec().PackageInfo.Version, + ) } } for _, req := range tx.Requirements { @@ -103,7 +148,7 @@ func printTransaction(tx update.UpdateTransaction) { _ = w.Flush() } -func handleOutput(pkgs []v1alpha1.ClusterPackage) { +func handleOutput(pkgs []ctrlpkg.Package) { if updateCmdOptions.Output == "" { return } @@ -111,7 +156,7 @@ func handleOutput(pkgs []v1alpha1.ClusterPackage) { var outputData []byte var err error for i := range pkgs { - if gvks, _, err := scheme.Scheme.ObjectKinds(&pkgs[i]); err == nil && len(gvks) == 1 { + if gvks, _, err := scheme.Scheme.ObjectKinds(pkgs[i]); err == nil && len(gvks) == 1 { pkgs[i].SetGroupVersionKind(gvks[0]) } else { fmt.Fprintf(os.Stderr, "āŒ failed to set GVK for package: %v\n", err) @@ -228,5 +273,7 @@ func init() { updateCmd.PersistentFlags().BoolVarP(&updateCmdOptions.Yes, "yes", "y", false, "do not ask for any confirmation") updateCmdOptions.OutputOptions.AddFlagsToCommand(updateCmd) + updateCmdOptions.KindOptions.AddFlagsToCommand(updateCmd) + updateCmdOptions.NamespaceOptions.AddFlagsToCommand(updateCmd) RootCmd.AddCommand(updateCmd) } diff --git a/cmd/glasskube/cmd/version.go b/cmd/glasskube/cmd/version.go index 498b82cfa..db9b167d8 100644 --- a/cmd/glasskube/cmd/version.go +++ b/cmd/glasskube/cmd/version.go @@ -4,10 +4,11 @@ import ( "fmt" "os" + "github.com/fatih/color" "github.com/glasskube/glasskube/internal/clientutils" - "github.com/glasskube/glasskube/internal/cliutils" "github.com/glasskube/glasskube/internal/config" + "github.com/glasskube/glasskube/internal/constants" "github.com/spf13/cobra" ) @@ -17,6 +18,7 @@ var versioncmd = &cobra.Command{ Long: `Print the version of glasskube and package-operator`, PreRun: cliutils.SetupClientContext(false, &rootCmdOptions.SkipUpdateCheck), Run: func(cmd *cobra.Command, args []string) { + color.Blue(constants.GlasskubeAscii) glasskubeVersion := config.Version fmt.Fprintf(os.Stderr, "glasskube: v%s\n", glasskubeVersion) operatorVersion, err := clientutils.GetPackageOperatorVersion(cmd.Context()) diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 4a440c3dd..5ef32f3e0 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -34,7 +34,7 @@ resources: images: - name: controller newName: ghcr.io/glasskube/package-operator - newTag: v0.10.1 # x-release-please-version + newTag: v0.11.0 # x-release-please-version # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 7af74cd83..8ace40455 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -5,4 +5,11 @@ var ( DaemonSet = "DaemonSet" StatefulSet = "StatefulSet" DefaultRepoUrl = "https://packages.dl.glasskube.dev/packages" + GlasskubeAscii = ` + ____ _ _ ____ ____ _ ___ _ ____ _____ + / ___| | / \ / ___/ ___|| |/ / | | | __ )| ____| +| | _| | / _ \ \___ \___ \| ' /| | | | _ \| _| +| |_| | |___ / ___ \ ___) |__) | . \| |_| | |_) | |___ + \____|_____/_/ \_\____/____/|_|\_\\___/|____/|_____| + ` ) diff --git a/internal/controller/ctrlpkg/util.go b/internal/controller/ctrlpkg/util.go new file mode 100644 index 000000000..eafd8e131 --- /dev/null +++ b/internal/controller/ctrlpkg/util.go @@ -0,0 +1,7 @@ +package ctrlpkg + +func IsSameResource(a, b Package) bool { + return a.GetName() == b.GetName() && + a.GroupVersionKind() == b.GroupVersionKind() && + a.GetNamespace() == b.GetNamespace() +} diff --git a/internal/dependency/manager.go b/internal/dependency/manager.go index 16a53a8f0..b290ebb5a 100644 --- a/internal/dependency/manager.go +++ b/internal/dependency/manager.go @@ -98,14 +98,13 @@ func (dm *DependendcyManager) NewGraph(ctx context.Context) (*graph.DependencyGr } } - // TODO re-enable this when we fully support namespaced packages (this is just a quickfix to avoid client-side rate limits) - /*if pkgs, err := dm.pkgClient.ListPackages(ctx, ""); err != nil { + if pkgs, err := dm.pkgClient.ListPackages(ctx, ""); err != nil { return nil, err } else { for i := range pkgs.Items { allPkgs = append(allPkgs, &pkgs.Items[i]) } - }*/ + } g := graph.NewGraph() for _, pkg := range allPkgs { diff --git a/internal/dependency/manager_test.go b/internal/dependency/manager_test.go index 6e4d0a06c..e0ccca84c 100644 --- a/internal/dependency/manager_test.go +++ b/internal/dependency/manager_test.go @@ -535,8 +535,7 @@ var _ = Describe("Dependency Manager", func() { BeforeEach(func() { ni.Status.Manifest.Dependencies = []v1alpha1.Dependency{{Name: d.Name, Version: "1.x.x"}} }) - // TODO re-enable - /*It("should prevent illegal update of D", func(ctx context.Context) { + It("should prevent illegal update of D", func(ctx context.Context) { d, di = createClusterPackageAndInfo("D", "2.0.0", false) res, err := dm.Validate(ctx, di.Status.Manifest, di.Spec.Version) Expect(err).ShouldNot(HaveOccurred()) @@ -544,7 +543,7 @@ var _ = Describe("Dependency Manager", func() { Expect(res.Status).Should(Equal(ValidationResultStatusConflict)) Expect(res.Requirements).Should(BeEmpty()) Expect(res.Conflicts).Should(HaveLen(1)) - })*/ + }) It("should allow legal update of D", func(ctx context.Context) { d, di = createClusterPackageAndInfo("D", "1.2.0", false) res, err := dm.Validate(ctx, di.Status.Manifest, di.Spec.Version) diff --git a/internal/manifestvalues/flags/flags.go b/internal/manifestvalues/flags/flags.go index 186323f76..75113ee81 100644 --- a/internal/manifestvalues/flags/flags.go +++ b/internal/manifestvalues/flags/flags.go @@ -39,7 +39,7 @@ func (opts *ValuesOptions) AddFlagsToCommand(cmd *cobra.Command) { flags.StringArrayVar(&opts.Values, "value", opts.Values, "set a value via flag (can be used multiple times).\n"+ "You can create values referencing data in other resources using the following syntax: "+ - "$$[specifier...].\n"+ + "$$[[,...]].\n"+ "For example:\n"+ " * Reference a ConfigMap key: --value \"name=$ConfigMapRef$namespace,name,key\"\n"+ " * Reference a Secret key: --value \"name=$SecretRef$namespace,name,key\"\n"+ diff --git a/internal/repo/types/types.go b/internal/repo/types/types.go index 47751854b..5619a1fae 100644 --- a/internal/repo/types/types.go +++ b/internal/repo/types/types.go @@ -1,5 +1,7 @@ package types +import "github.com/glasskube/glasskube/api/v1alpha1" + type PackageIndex struct { Versions []PackageIndexItem `json:"versions" jsonschema:"required"` LatestVersion string `json:"latestVersion" jsonschema:"required"` @@ -14,10 +16,11 @@ type PackageRepoIndex struct { } type PackageRepoIndexItem struct { - Name string `json:"name"` - ShortDescription string `json:"shortDescription,omitempty"` - IconUrl string `json:"iconUrl,omitempty"` - LatestVersion string `json:"latestVersion,omitempty"` + Name string `json:"name"` + ShortDescription string `json:"shortDescription,omitempty"` + IconUrl string `json:"iconUrl,omitempty"` + LatestVersion string `json:"latestVersion,omitempty"` + Scope *v1alpha1.PackageScope `json:"scope,omitempty"` } type MetaIndex struct { diff --git a/internal/telemetry/client.go b/internal/telemetry/client.go index 0153fe815..555f86a3b 100644 --- a/internal/telemetry/client.go +++ b/internal/telemetry/client.go @@ -225,7 +225,7 @@ func HttpMiddleware() func(http.Handler) http.Handler { ev := instance.getBaseEvent("ui_endpoint", false) ev.Properties["$current_url"] = r.URL.String() ev.Properties["method"] = r.Method - ev.Properties["path"] = r.URL + ev.Properties["path"] = r.URL // TODO exclusions necessary since namespace/name are part of the URL! ev.Properties["execution_time"] = time.Since(start).Milliseconds() ev.Properties["user_agent"] = r.UserAgent() _ = instance.posthog.Enqueue(ev) diff --git a/internal/web/components/pkg_config_input/controller.go b/internal/web/components/pkg_config_input/controller.go index 61692846f..d37aded05 100644 --- a/internal/web/components/pkg_config_input/controller.go +++ b/internal/web/components/pkg_config_input/controller.go @@ -4,6 +4,10 @@ import ( "fmt" "strconv" + "github.com/glasskube/glasskube/internal/web/util" + + "github.com/glasskube/glasskube/internal/controller/ctrlpkg" + "github.com/glasskube/glasskube/api/v1alpha1" ) @@ -34,11 +38,12 @@ type pkgConfigInputInput struct { ValueError error Autofocus bool DatalistOptions *PkgConfigInputDatalistOptions + PackageHref string } -func getStringValue(pkg *v1alpha1.ClusterPackage, valueName string, valueDefinition *v1alpha1.ValueDefinition) string { - if pkg != nil { - if valueConfiguration, ok := pkg.Spec.Values[valueName]; ok { +func getStringValue(pkg ctrlpkg.Package, valueName string, valueDefinition *v1alpha1.ValueDefinition) string { + if !pkg.IsNil() { + if valueConfiguration, ok := pkg.GetSpec().Values[valueName]; ok { if valueConfiguration.Value != nil { return *valueConfiguration.Value } @@ -47,7 +52,7 @@ func getStringValue(pkg *v1alpha1.ClusterPackage, valueName string, valueDefinit return valueDefinition.DefaultValue } -func getBoolValue(pkg *v1alpha1.ClusterPackage, valueName string, valueDefinition *v1alpha1.ValueDefinition) bool { +func getBoolValue(pkg ctrlpkg.Package, valueName string, valueDefinition *v1alpha1.ValueDefinition) bool { if valueDefinition.Type == v1alpha1.ValueTypeBoolean { strVal := getStringValue(pkg, valueName, valueDefinition) if valBool, err := strconv.ParseBool(strVal); err == nil { @@ -65,9 +70,9 @@ func getLabel(valueName string, valueDefinition *v1alpha1.ValueDefinition) strin return inputLabel } -func getExistingReferenceAndKind(pkg *v1alpha1.ClusterPackage, valueName string) (*v1alpha1.ValueReference, string) { - if pkg != nil { - if val, ok := pkg.Spec.Values[valueName]; ok { +func getExistingReferenceAndKind(pkg ctrlpkg.Package, valueName string) (*v1alpha1.ValueReference, string) { + if !pkg.IsNil() { + if val, ok := pkg.GetSpec().Values[valueName]; ok { if val.Value == nil && val.ValueFrom != nil { if val.ValueFrom.ConfigMapRef != nil { return val.ValueFrom, "ConfigMap" @@ -83,7 +88,7 @@ func getExistingReferenceAndKind(pkg *v1alpha1.ClusterPackage, valueName string) } func getOrCreateReference( - pkg *v1alpha1.ClusterPackage, valueName string, desiredRefKind *string) (v1alpha1.ValueReference, string) { + pkg ctrlpkg.Package, valueName string, desiredRefKind *string) (v1alpha1.ValueReference, string) { existingReference, existingRefKind := getExistingReferenceAndKind(pkg, valueName) if desiredRefKind != nil && *desiredRefKind != existingRefKind { return v1alpha1.ValueReference{}, *desiredRefKind @@ -95,10 +100,10 @@ func getOrCreateReference( } func ForPkgConfigInput( - pkg *v1alpha1.ClusterPackage, + pkg ctrlpkg.Package, repositoryName string, selectedVersion string, - pkgName string, + manifest *v1alpha1.PackageManifest, valueName string, valueDefinition v1alpha1.ValueDefinition, valueError error, @@ -112,7 +117,7 @@ func ForPkgConfigInput( return &pkgConfigInputInput{ RepositoryName: repositoryName, SelectedVersion: selectedVersion, - PkgName: pkgName, + PkgName: manifest.Name, ValueName: valueName, ValueDefinition: valueDefinition, StringValue: getStringValue(pkg, valueName, &valueDefinition), @@ -125,5 +130,6 @@ func ForPkgConfigInput( ValueError: valueError, Autofocus: options.Autofocus, DatalistOptions: datalistOptions, + PackageHref: util.GetPackageHrefWithFallback(pkg, manifest), } } diff --git a/internal/web/components/pkg_detail_btns/controller.go b/internal/web/components/pkg_detail_btns/controller.go index 65483da3e..dbf33f1d2 100644 --- a/internal/web/components/pkg_detail_btns/controller.go +++ b/internal/web/components/pkg_detail_btns/controller.go @@ -3,6 +3,10 @@ package pkg_detail_btns import ( "fmt" + "github.com/glasskube/glasskube/internal/web/util" + + "github.com/glasskube/glasskube/internal/controller/ctrlpkg" + "github.com/glasskube/glasskube/api/v1alpha1" "github.com/glasskube/glasskube/pkg/client" ) @@ -15,7 +19,8 @@ type pkgDetailBtnsInput struct { Status *client.PackageStatus Manifest *v1alpha1.PackageManifest UpdateAvailable bool - Pkg *v1alpha1.ClusterPackage + Pkg ctrlpkg.Package + PackageHref string } func getId(pkgName string) string { @@ -26,7 +31,7 @@ func ForPkgDetailBtns( pkgName string, status *client.PackageStatus, manifest *v1alpha1.PackageManifest, - pkg *v1alpha1.ClusterPackage, + pkg ctrlpkg.Package, updateAvailable bool, ) *pkgDetailBtnsInput { id := getId(pkgName) @@ -37,5 +42,6 @@ func ForPkgDetailBtns( Manifest: manifest, UpdateAvailable: updateAvailable, Pkg: pkg, + PackageHref: util.GetPackageHref(pkg, manifest), } } diff --git a/internal/web/components/pkg_overview_btn/controller.go b/internal/web/components/pkg_overview_btn/controller.go index 8c76536e8..d1fbea729 100644 --- a/internal/web/components/pkg_overview_btn/controller.go +++ b/internal/web/components/pkg_overview_btn/controller.go @@ -3,34 +3,42 @@ package pkg_overview_btn import ( "fmt" + "github.com/glasskube/glasskube/internal/web/util" + "github.com/glasskube/glasskube/api/v1alpha1" "github.com/glasskube/glasskube/pkg/client" "github.com/glasskube/glasskube/pkg/list" ) -const TemplateId = "pkg-overview-btn" +const templateId = "clpkg-overview-btn" -type pkgOverviewBtnInput struct { +type clpkgOverviewBtnInput struct { ButtonId string PackageName string Status *client.PackageStatus Manifest *v1alpha1.PackageManifest UpdateAvailable bool - Pkg *v1alpha1.ClusterPackage + InDeletion bool + PackageHref string } func getButtonId(pkgName string) string { - return fmt.Sprintf("%v-%v", TemplateId, pkgName) + return fmt.Sprintf("%v-%v", templateId, pkgName) } -func ForPkgOverviewBtn(packageWithStatus *list.PackageWithStatus, updateAvailable bool) *pkgOverviewBtnInput { +func ForClPkgOverviewBtn(packageWithStatus *list.PackageWithStatus, updateAvailable bool) *clpkgOverviewBtnInput { buttonId := getButtonId(packageWithStatus.Name) - return &pkgOverviewBtnInput{ + inDeletion := false + if packageWithStatus.ClusterPackage != nil { + inDeletion = !packageWithStatus.ClusterPackage.DeletionTimestamp.IsZero() + } + return &clpkgOverviewBtnInput{ ButtonId: buttonId, PackageName: packageWithStatus.Name, Status: packageWithStatus.Status, Manifest: packageWithStatus.InstalledManifest, UpdateAvailable: updateAvailable, - Pkg: packageWithStatus.Package, + InDeletion: inDeletion, + PackageHref: util.GetClusterPkgHref(packageWithStatus.Name), } } diff --git a/internal/web/components/pkg_update_alert/controller.go b/internal/web/components/pkg_update_alert/controller.go index ea816c692..8c4481c24 100644 --- a/internal/web/components/pkg_update_alert/controller.go +++ b/internal/web/components/pkg_update_alert/controller.go @@ -4,8 +4,12 @@ const TemplateId = "pkg-update-alert" type pkgUpdateAlertInput struct { UpdatesAvailable bool + PackageHref string } func ForPkgUpdateAlert(data map[string]any) *pkgUpdateAlertInput { - return &pkgUpdateAlertInput{UpdatesAvailable: data["UpdatesAvailable"].(bool)} + return &pkgUpdateAlertInput{ + UpdatesAvailable: data["UpdatesAvailable"].(bool), + PackageHref: data["PackageHref"].(string), + } } diff --git a/internal/web/configurationvalues.go b/internal/web/configurationvalues.go index ce97eabb7..257ea1e34 100644 --- a/internal/web/configurationvalues.go +++ b/internal/web/configurationvalues.go @@ -100,31 +100,64 @@ func extractPackageValueSource(r *http.Request, valueName string) *v1alpha1.Pack } } -// packageConfigurationInput is a GET endpoint, which returns an html snippet containing an input container. +// packageConfigurationInput is like clusterPackageConfigurationInput but for packages +func (s *server) packageConfigurationInput(w http.ResponseWriter, r *http.Request) { + manifestName := mux.Vars(r)["manifestName"] + namespace := mux.Vars(r)["namespace"] + name := mux.Vars(r)["name"] + selectedVersion := r.FormValue("selectedVersion") + repositoryName := r.FormValue("repositoryName") + pkg, manifest, err := describe.DescribeInstalledPackage(r.Context(), namespace, name) + if err != nil && !errors.IsNotFound(err) { + err = fmt.Errorf("an error occurred fetching package details of %v: %w", manifestName, err) + fmt.Fprintf(os.Stderr, "%v\n", err) + return + } + + s.handleConfigurationInput(w, r, &packageDetailPageContext{ + repositoryName: repositoryName, + selectedVersion: selectedVersion, + manifestName: manifestName, + pkg: pkg, + manifest: manifest, + }) +} + +// clusterPackageConfigurationInput is a GET endpoint, which returns an html snippet containing an input container. // The endpoint requires the pkgName query parameter to be set, as well as the valueName query parameter (which holds // the name of the desired value according to the package value definitions). // An optional query parameter refKind can be passed to request the snippet in a certain variant, where the accepted // refKind values are: ConfigMap, Secret, Package. If no refKind is given, the "regular" input is returned. // In any case, the input container consists of a button where the user can change the type of reference or remove the // reference, and the actual input field(s). -func (s *server) packageConfigurationInput(w http.ResponseWriter, r *http.Request) { +func (s *server) clusterPackageConfigurationInput(w http.ResponseWriter, r *http.Request) { pkgName := mux.Vars(r)["pkgName"] selectedVersion := r.FormValue("selectedVersion") repositoryName := r.FormValue("repositoryName") - pkg, manifest, err := describe.DescribeInstalledPackage(r.Context(), pkgName) + pkg, manifest, err := describe.DescribeInstalledClusterPackage(r.Context(), pkgName) if err != nil && !errors.IsNotFound(err) { err = fmt.Errorf("an error occurred fetching package details of %v: %w", pkgName, err) fmt.Fprintf(os.Stderr, "%v\n", err) return } - if manifest == nil { - manifest = &v1alpha1.PackageManifest{} - if err := s.repoClientset.ForRepoWithName(repositoryName). - FetchPackageManifest(pkgName, selectedVersion, manifest); err != nil { + s.handleConfigurationInput(w, r, &packageDetailPageContext{ + repositoryName: repositoryName, + selectedVersion: selectedVersion, + manifestName: pkgName, + pkg: pkg, + manifest: manifest, + }) +} + +func (s *server) handleConfigurationInput(w http.ResponseWriter, r *http.Request, d *packageDetailPageContext) { + if d.manifest == nil { + d.manifest = &v1alpha1.PackageManifest{} + if err := s.repoClientset.ForRepoWithName(d.repositoryName). + FetchPackageManifest(d.manifestName, d.selectedVersion, d.manifest); err != nil { // TODO check error handling again? s.respondAlertAndLog(w, err, - fmt.Sprintf("An error occurred fetching manifest of %v in version %v", pkgName, selectedVersion), + fmt.Sprintf("An error occurred fetching manifest of %v in version %v", d.manifestName, d.selectedVersion), "danger") return } @@ -132,7 +165,7 @@ func (s *server) packageConfigurationInput(w http.ResponseWriter, r *http.Reques valueName := mux.Vars(r)["valueName"] refKind := r.URL.Query().Get("refKind") - if valueDefinition, ok := manifest.ValueDefinitions[valueName]; ok { + if valueDefinition, ok := d.manifest.ValueDefinitions[valueName]; ok { options := pkg_config_input.PkgConfigInputDatalistOptions{} if refKind == refKindConfigMap || refKind == refKindSecret { if opts, err := s.getNamespaceOptions(); err != nil { @@ -148,13 +181,13 @@ func (s *server) packageConfigurationInput(w http.ResponseWriter, r *http.Reques } } input := pkg_config_input.ForPkgConfigInput( - pkg, repositoryName, selectedVersion, pkgName, valueName, valueDefinition, nil, &options, + d.pkg, d.repositoryName, d.selectedVersion, d.manifest, valueName, valueDefinition, nil, &options, &pkg_config_input.PkgConfigInputRenderOptions{ Autofocus: true, DesiredRefKind: &refKind, }) - err = s.templates.pkgConfigInput.Execute(w, input) - checkTmplError(err, fmt.Sprintf("package config input (%s, %s)", pkgName, valueName)) + err := s.templates.pkgConfigInput.Execute(w, input) + checkTmplError(err, fmt.Sprintf("package config input (%s, %s)", d.manifestName, valueName)) } } diff --git a/internal/web/discussions.go b/internal/web/discussions.go index 791f38fa2..ea3d7d00d 100644 --- a/internal/web/discussions.go +++ b/internal/web/discussions.go @@ -5,6 +5,8 @@ import ( "net/http" "os" + "github.com/glasskube/glasskube/internal/web/util" + "github.com/glasskube/glasskube/internal/giscus" "github.com/glasskube/glasskube/internal/httperror" @@ -20,53 +22,96 @@ import ( // packageDiscussion is a full page for showing various discussions, reactions, etc. func (s *server) packageDiscussion(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { - githubUrl := r.FormValue("githubUrl") - telemetry.SetUserProperty("github_url", githubUrl) + s.handleGiscus(r) + return + } + manifestName := mux.Vars(r)["manifestName"] + namespace := mux.Vars(r)["namespace"] + name := mux.Vars(r)["name"] + repositoryName := mux.Vars(r)["repositoryName"] + pkg, manifest, err := describe.DescribeInstalledPackage(r.Context(), namespace, name) + if err != nil && !errors.IsNotFound(err) { + s.respondAlertAndLog(w, err, + fmt.Sprintf("An error occurred fetching installed package %v", name), "danger") + return + } + + s.handlePackageDiscussionPage(w, r, &packageDetailPageContext{ + repositoryName: repositoryName, + manifestName: manifestName, + pkg: pkg, + manifest: manifest, + }) + +} + +// clusterPackageDiscussion is a full page for showing various discussions, reactions, etc. +func (s *server) clusterPackageDiscussion(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + s.handleGiscus(r) return } pkgName := mux.Vars(r)["pkgName"] repositoryName := mux.Vars(r)["repositoryName"] - pkg, manifest, err := describe.DescribeInstalledPackage(r.Context(), pkgName) + pkg, manifest, err := describe.DescribeInstalledClusterPackage(r.Context(), pkgName) if err != nil && !errors.IsNotFound(err) { s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching installed package %v", pkgName), "danger") return - } else if err != nil { - // implies that the package is not installed - err = nil } + s.handlePackageDiscussionPage(w, r, &packageDetailPageContext{ + repositoryName: repositoryName, + manifestName: pkgName, + pkg: pkg, + manifest: manifest, + }) +} + +func (s *server) handleGiscus(r *http.Request) { + githubUrl := r.FormValue("githubUrl") + telemetry.SetUserProperty("github_url", githubUrl) +} + +func (s *server) handlePackageDiscussionPage(w http.ResponseWriter, r *http.Request, d *packageDetailPageContext) { var idx repo.PackageIndex - if err := s.repoClientset.ForRepoWithName(repositoryName).FetchPackageIndex(pkgName, &idx); err != nil { - s.respondAlertAndLog(w, err, "An error occurred fetching versions of "+pkgName, "danger") + if err := s.repoClientset.ForRepoWithName(d.repositoryName).FetchPackageIndex(d.manifestName, &idx); err != nil { + s.respondAlertAndLog(w, err, "An error occurred fetching versions of "+d.manifestName, "danger") return } - if manifest == nil { - manifest = &v1alpha1.PackageManifest{} - if err := s.repoClientset.ForRepoWithName(repositoryName). - FetchPackageManifest(pkgName, idx.LatestVersion, manifest); err != nil { + if d.manifest == nil { + d.manifest = &v1alpha1.PackageManifest{} + if err := s.repoClientset.ForRepoWithName(d.repositoryName). + FetchPackageManifest(d.manifestName, idx.LatestVersion, d.manifest); err != nil { s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching manifest of %v in version %v in repository %v", - pkgName, idx.LatestVersion, repositoryName), "danger") + d.manifest, idx.LatestVersion, d.repositoryName), "danger") return } } - err = s.templates.pkgDiscussionPageTmpl.Execute(w, s.enrichPage(r, map[string]any{ + pkgHref := util.GetPackageHref(d.pkg, d.manifest) + + err := s.templates.pkgDiscussionPageTmpl.Execute(w, s.enrichPage(r, map[string]any{ "Giscus": giscus.Client().Config, - "Package": pkg, - "Status": client.GetStatusOrPending(pkg), - "Manifest": manifest, + "Package": d.pkg, + "Status": client.GetStatusOrPending(d.pkg), + "Manifest": d.manifest, "LatestVersion": idx.LatestVersion, - "UpdateAvailable": pkg != nil && s.isUpdateAvailable(r.Context(), pkgName), + "UpdateAvailable": s.isUpdateAvailableForPkg(r.Context(), d.pkg), "ShowDiscussionLink": true, - }, err)) - checkTmplError(err, fmt.Sprintf("package-detail (%s)", pkgName)) + "PackageHref": pkgHref, + "DiscussionHref": fmt.Sprintf("%s/discussion", pkgHref), + }, nil)) + checkTmplError(err, fmt.Sprintf("package-discussion (%s)", d.manifestName)) } func (s *server) discussionBadge(w http.ResponseWriter, r *http.Request) { pkgName := mux.Vars(r)["pkgName"] + if pkgName == "" { + pkgName = mux.Vars(r)["manifestName"] + } var totalCount int if counts, err := giscus.Client().GetCountsFor(pkgName); err != nil { diff --git a/internal/web/packagedetail.go b/internal/web/packagedetail.go new file mode 100644 index 000000000..a0cfe694b --- /dev/null +++ b/internal/web/packagedetail.go @@ -0,0 +1,217 @@ +package web + +import ( + "context" + "fmt" + "net/http" + "os" + "slices" + + webutil "github.com/glasskube/glasskube/internal/web/util" + + "github.com/glasskube/glasskube/api/v1alpha1" + "github.com/glasskube/glasskube/internal/clientutils" + "github.com/glasskube/glasskube/internal/controller/ctrlpkg" + "github.com/glasskube/glasskube/internal/dependency" + "github.com/glasskube/glasskube/internal/repo" + "github.com/glasskube/glasskube/internal/repo/types" + "github.com/glasskube/glasskube/internal/util" + "github.com/glasskube/glasskube/internal/web/components/pkg_config_input" + "github.com/glasskube/glasskube/pkg/client" + "github.com/glasskube/glasskube/pkg/describe" + "github.com/gorilla/mux" + "k8s.io/apimachinery/pkg/api/errors" +) + +type packageDetailPageContext struct { + repositoryName string + selectedVersion string + manifestName string + pkg ctrlpkg.Package + manifest *v1alpha1.PackageManifest +} + +func (s *server) packageDetail(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + manifestName := mux.Vars(r)["manifestName"] + namespace := mux.Vars(r)["namespace"] + name := mux.Vars(r)["name"] + repositoryName := r.FormValue("repositoryName") + selectedVersion := r.FormValue("selectedVersion") + + var pkg *v1alpha1.Package + var manifest *v1alpha1.PackageManifest + if namespace != "" && name != "" && namespace != "-" && name != "-" { + var err error + pkg, manifest, err = describe.DescribeInstalledPackage(ctx, namespace, name) + if err != nil && !errors.IsNotFound(err) { + s.respondAlertAndLog(w, err, + fmt.Sprintf("An error occurred fetching package details of installed package %v in namespace %v", name, namespace), + "danger") + return + } else if errors.IsNotFound(err) { + s.swappingRedirect(w, "/packages", "main", "main") + w.WriteHeader(http.StatusNotFound) + return + } else if pkg != nil { + repositoryName = pkg.Spec.PackageInfo.RepositoryName + } + } + + s.handlePackageDetailPage(ctx, &packageDetailPageContext{ + repositoryName: repositoryName, + selectedVersion: selectedVersion, + manifestName: manifestName, + pkg: pkg, + manifest: manifest, + }, r, w) +} + +func (s *server) clusterPackageDetail(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pkgName := mux.Vars(r)["pkgName"] + repositoryName := r.FormValue("repositoryName") + selectedVersion := r.FormValue("selectedVersion") + + pkg, manifest, err := describe.DescribeInstalledClusterPackage(ctx, pkgName) + if err != nil && !errors.IsNotFound(err) { + s.respondAlertAndLog(w, err, + fmt.Sprintf("An error occurred fetching package details of installed package %v", pkgName), + "danger") + return + } else if pkg != nil { + repositoryName = pkg.Spec.PackageInfo.RepositoryName + } + + s.handlePackageDetailPage(ctx, &packageDetailPageContext{ + repositoryName: repositoryName, + selectedVersion: selectedVersion, + manifestName: pkgName, + pkg: pkg, + manifest: manifest, + }, r, w) +} + +func (s *server) handlePackageDetailPage(ctx context.Context, d *packageDetailPageContext, r *http.Request, w http.ResponseWriter) { + var err error + var repos []v1alpha1.PackageRepository + var usedRepo *v1alpha1.PackageRepository + if d.repositoryName, repos, usedRepo, err = s.getRepos( + ctx, d.manifestName, d.repositoryName); err != nil { + s.respondAlertAndLog(w, err, "", "danger") + return + } + + var idx repo.PackageIndex + var latestVersion string + if idx, latestVersion, d.selectedVersion, err = s.getVersions( + d.repositoryName, d.manifestName, d.selectedVersion); err != nil { + s.respondAlertAndLog(w, err, + fmt.Sprintf("An error occurred fetching package index of %v in repository %v", d.manifestName, d.repositoryName), + "danger") + return + } + + if d.manifest == nil { + d.manifest = &v1alpha1.PackageManifest{} + if err := s.repoClientset.ForRepoWithName(d.repositoryName). + FetchPackageManifest(d.manifestName, d.selectedVersion, d.manifest); err != nil { + s.respondAlertAndLog(w, err, + fmt.Sprintf("An error occurred fetching manifest of %v in version %v in repository %v", + d.manifestName, d.selectedVersion, d.repositoryName), + "danger") + return + } + } + + res, err := s.dependencyMgr.Validate(r.Context(), d.manifest, d.selectedVersion) + if err != nil { + s.respondAlertAndLog(w, err, + fmt.Sprintf("An error occurred validating dependencies of %v in version %v", d.manifestName, d.selectedVersion), + "danger") + return + } + + valueErrors := make(map[string]error) + datalistOptions := make(map[string]*pkg_config_input.PkgConfigInputDatalistOptions) + if !d.pkg.IsNil() { + nsOptions, _ := s.getNamespaceOptions() + pkgsOptions, _ := s.getPackagesOptions(r.Context()) + for key, v := range d.pkg.GetSpec().Values { + if _, err := s.valueResolver.ResolveValue(r.Context(), v); err != nil { + valueErrors[key] = util.GetRootCause(err) + } + if v.ValueFrom != nil { + options, err := s.getDatalistOptions(r.Context(), v.ValueFrom, nsOptions, pkgsOptions) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + } + datalistOptions[key] = options + } + } + } + + err = s.templates.pkgPageTmpl.Execute(w, s.enrichPage(r, map[string]any{ + "Package": d.pkg, + "Status": client.GetStatusOrPending(d.pkg), + "Manifest": d.manifest, + "LatestVersion": latestVersion, + "UpdateAvailable": s.isUpdateAvailableForPkg(r.Context(), d.pkg), + "AutoUpdate": clientutils.AutoUpdateString(d.pkg, "Disabled"), + "ValidationResult": res, + "ShowConflicts": res.Status == dependency.ValidationResultStatusConflict, + "SelectedVersion": d.selectedVersion, + "PackageIndex": &idx, + "Repositories": repos, + "RepositoryName": d.repositoryName, + "ShowConfiguration": (!d.pkg.IsNil() && len(d.manifest.ValueDefinitions) > 0 && d.pkg.GetDeletionTimestamp().IsZero()) || d.pkg.IsNil(), + "ValueErrors": valueErrors, + "DatalistOptions": datalistOptions, + "ShowDiscussionLink": usedRepo.IsGlasskubeRepo(), + "PackageHref": webutil.GetPackageHrefWithFallback(d.pkg, d.manifest), + }, err)) + checkTmplError(err, fmt.Sprintf("package-detail (%s)", d.manifestName)) +} + +func (s *server) getVersions(repositoryName string, pkgName string, selectedVersion string) (repo.PackageIndex, string, string, error) { + var idx repo.PackageIndex + if err := s.repoClientset.ForRepoWithName(repositoryName).FetchPackageIndex(pkgName, &idx); err != nil { + return repo.PackageIndex{}, "", "", err + } + latestVersion := idx.LatestVersion + + if selectedVersion == "" { + selectedVersion = latestVersion + } else if !slices.ContainsFunc(idx.Versions, func(item types.PackageIndexItem) bool { + return item.Version == selectedVersion + }) { + selectedVersion = latestVersion + } + return idx, latestVersion, selectedVersion, nil +} + +func (s *server) getRepos(ctx context.Context, manifestName string, repositoryName string) ( + string, []v1alpha1.PackageRepository, *v1alpha1.PackageRepository, error) { + var repos []v1alpha1.PackageRepository + var err error + if repos, err = s.repoClientset.Meta().GetReposForPackage(manifestName); err != nil { + fmt.Fprintf(os.Stderr, "error getting repos for package; %v", err) + } else if repositoryName == "" { + if len(repos) == 0 { + return "", nil, nil, fmt.Errorf("%v not found in any repository", manifestName) + } + for _, r := range repos { + repositoryName = r.Name + if r.IsDefaultRepository() { + break + } + } + } + + var usedRepo v1alpha1.PackageRepository + if err := s.pkgClient.PackageRepositories().Get(ctx, repositoryName, &usedRepo); err != nil { + return "", nil, nil, err + } + + return repositoryName, repos, &usedRepo, nil +} diff --git a/internal/web/response.go b/internal/web/response.go new file mode 100644 index 000000000..e7648b4ff --- /dev/null +++ b/internal/web/response.go @@ -0,0 +1,50 @@ +package web + +import ( + "encoding/json" + "fmt" + "net/http" + "os" +) + +func (s *server) respondSuccess(w http.ResponseWriter) { + err := s.templates.alertTmpl.Execute(w, map[string]any{ + "Message": "Configuration updated successfully", + "Dismissible": true, + "Type": "success", + }) + checkTmplError(err, "success") +} + +func (s *server) respondAlertAndLog(w http.ResponseWriter, err error, wrappingMsg string, alertType string) { + if wrappingMsg != "" { + err = fmt.Errorf("%v: %w", wrappingMsg, err) + } + fmt.Fprintf(os.Stderr, "%v\n", err) + s.respondAlert(w, err.Error(), alertType) +} + +func (s *server) respondAlert(w http.ResponseWriter, message string, alertType string) { + w.Header().Add("Hx-Reselect", "div.alert") // overwrite any existing hx-select (which was a little intransparent sometimes) + w.Header().Add("Hx-Reswap", "afterbegin") + w.WriteHeader(http.StatusBadRequest) + err := s.templates.alertTmpl.Execute(w, map[string]any{ + "Message": message, + "Dismissible": true, + "Type": alertType, + }) + checkTmplError(err, "alert") +} + +// swappingRedirect adds the Hx-Location header to the response, which, when interpreted by htmx.js, will make +// the frontend redirect to the given path and swap the given target with the given slect from the response +// also see: https://htmx.org/headers/hx-location/ +func (s *server) swappingRedirect(w http.ResponseWriter, path string, target string, slect string) { + locationData := map[string]string{ + "path": path, + "target": target, + "select": slect, + } + locationJson, _ := json.Marshal(locationData) + w.Header().Add("Hx-Location", string(locationJson)) +} diff --git a/internal/web/server.go b/internal/web/server.go index fccad1923..687214cad 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -18,6 +18,10 @@ import ( "sync" "syscall" + "github.com/glasskube/glasskube/internal/controller/ctrlpkg" + + "github.com/glasskube/glasskube/internal/web/util" + "github.com/Masterminds/semver/v3" "github.com/glasskube/glasskube/api/v1alpha1" clientadapter "github.com/glasskube/glasskube/internal/adapter/goclient" @@ -28,10 +32,8 @@ import ( "github.com/glasskube/glasskube/internal/manifestvalues" "github.com/glasskube/glasskube/internal/repo" repoclient "github.com/glasskube/glasskube/internal/repo/client" - "github.com/glasskube/glasskube/internal/repo/types" + repotypes "github.com/glasskube/glasskube/internal/repo/types" "github.com/glasskube/glasskube/internal/telemetry" - "github.com/glasskube/glasskube/internal/util" - "github.com/glasskube/glasskube/internal/web/components/pkg_config_input" "github.com/glasskube/glasskube/internal/web/handler" "github.com/glasskube/glasskube/pkg/bootstrap" "github.com/glasskube/glasskube/pkg/client" @@ -174,23 +176,49 @@ func (s *server) Start(ctx context.Context) error { router.HandleFunc("/kubeconfig", s.kubeconfigPage) router.Handle("/bootstrap", s.requireKubeconfig(s.bootstrapPage)) router.Handle("/kubeconfig/persist", s.requireKubeconfig(s.persistKubeconfig)) + // overview pages router.Handle("/packages", s.requireReady(s.packages)) - router.Handle("/packages/update", s.requireReady(s.update)) - router.Handle("/packages/update/modal", s.requireReady(s.updateModal)) - router.Handle("/packages/uninstall", s.requireReady(s.uninstall)) - router.Handle("/packages/uninstall/modal", s.requireReady(s.uninstallModal)) - router.Handle("/packages/open", s.requireReady(s.open)) - router.Handle("/packages/{pkgName}", s.requireReady(s.packageDetail)) - router.Handle("/packages/{pkgName}/discussion", s.requireReady(s.packageDiscussion)) - router.Handle("/packages/{pkgName}/discussion/badge", s.requireReady(s.discussionBadge)) - router.Handle("/packages/{pkgName}/configure", s.requireReady(s.installOrConfigurePackage)) - router.Handle("/packages/{pkgName}/configure/advanced", s.requireReady(s.advancedConfiguration)) - router.Handle("/packages/{pkgName}/configuration/{valueName}", s.requireReady(s.packageConfigurationInput)) - router.Handle("/packages/{pkgName}/configuration/{valueName}/datalists/names", s.requireReady(s.namesDatalist)) - router.Handle("/packages/{pkgName}/configuration/{valueName}/datalists/keys", s.requireReady(s.keysDatalist)) + router.Handle("/clusterpackages", s.requireReady(s.clusterPackages)) + + // detail page endpoints + pkgBasePath := "/packages/{manifestName}" + installedPkgBasePath := pkgBasePath + "/{namespace}/{name}" + clpkgBasePath := "/clusterpackages/{pkgName}" + router.Handle(pkgBasePath, s.requireReady(s.packageDetail)) + router.Handle(installedPkgBasePath, s.requireReady(s.packageDetail)) + router.Handle(clpkgBasePath, s.requireReady(s.clusterPackageDetail)) + // discussion endpoints + router.Handle(pkgBasePath+"/discussion", s.requireReady(s.packageDiscussion)) + router.Handle(installedPkgBasePath+"/discussion", s.requireReady(s.packageDiscussion)) + router.Handle(clpkgBasePath+"/discussion", s.requireReady(s.clusterPackageDiscussion)) + router.Handle(pkgBasePath+"/discussion/badge", s.requireReady(s.discussionBadge)) + router.Handle(installedPkgBasePath+"/discussion/badge", s.requireReady(s.discussionBadge)) + router.Handle(clpkgBasePath+"/discussion/badge", s.requireReady(s.discussionBadge)) + // configuration endpoints + router.Handle(installedPkgBasePath+"/configure", s.requireReady(s.installOrConfigurePackage)) + router.Handle(clpkgBasePath+"/configure", s.requireReady(s.installOrConfigureClusterPackage)) + router.Handle(installedPkgBasePath+"/configure/advanced", s.requireReady(s.advancedPackageConfiguration)) + router.Handle(clpkgBasePath+"/configure/advanced", s.requireReady(s.advancedClusterPackageConfiguration)) + router.Handle(pkgBasePath+"/configuration/{valueName}", s.requireReady(s.packageConfigurationInput)) + router.Handle(installedPkgBasePath+"/configuration/{valueName}", s.requireReady(s.packageConfigurationInput)) + router.Handle(clpkgBasePath+"/configuration/{valueName}", s.requireReady(s.clusterPackageConfigurationInput)) + // update endpoints + router.Handle(installedPkgBasePath+"/update", s.requireReady(s.update)) + router.Handle(clpkgBasePath+"/update", s.requireReady(s.update)) + // open endpoints + router.Handle(installedPkgBasePath+"/open", s.requireReady(s.open)) + router.Handle(clpkgBasePath+"/open", s.requireReady(s.open)) + // uninstall endpoints + router.Handle(installedPkgBasePath+"/uninstall", s.requireReady(s.uninstall)) + router.Handle(clpkgBasePath+"/uninstall", s.requireReady(s.uninstall)) + + // configuration datalist endpoints + router.Handle("/datalists/{valueName}/names", s.requireReady(s.namesDatalist)) + router.Handle("/datalists/{valueName}/keys", s.requireReady(s.keysDatalist)) + // settings router.Handle("/settings", s.requireReady(s.settingsPage)) router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/packages", http.StatusFound) + http.Redirect(w, r, "/clusterpackages", http.StatusFound) }) http.Handle("/", s.enrichContext(router)) @@ -232,351 +260,388 @@ func (s *server) Start(ctx context.Context) error { return nil } -func (s *server) updateModal(w http.ResponseWriter, r *http.Request) { +// uninstall is an endpoint, which returns the modal html for GET requests, and performs the update for POST +func (s *server) update(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - pkgName := r.FormValue("packageName") - pkgs := make([]string, 0, 1) - if pkgName != "" { - pkgs = append(pkgs, pkgName) - } + pkgName := mux.Vars(r)["pkgName"] + manifestName := mux.Vars(r)["manifestName"] + namespace := mux.Vars(r)["namespace"] + name := mux.Vars(r)["name"] - updates := make([]map[string]any, 0) - updater := update.NewUpdater(ctx).WithStatusWriter(statuswriter.Stderr()) - ut, err := updater.Prepare(ctx, pkgs) - if err != nil { - s.respondAlertAndLog(w, err, "An error occurred preparing update of "+pkgName, "danger") - return - } - utId := rand.Int() - s.updateMutex.Lock() - s.updateTransactions[utId] = *ut - s.updateMutex.Unlock() + if r.Method == http.MethodPost { + updater := update.NewUpdater(ctx).WithStatusWriter(statuswriter.Stderr()) + s.updateMutex.Lock() + defer s.updateMutex.Unlock() + utIdStr := r.FormValue("updateTransactionId") + if utId, err := strconv.Atoi(utIdStr); err != nil { + s.respondAlertAndLog(w, err, "Failed to parse updateTransactionId", "danger") + return + } else if ut, ok := s.updateTransactions[utId]; !ok { + s.respondAlert(w, fmt.Sprintf("Failed to find UpdateTransaction with ID %d", utId), "danger") + return + } else if _, err := updater.Apply(ctx, &ut); err != nil { + delete(s.updateTransactions, utId) + s.respondAlertAndLog(w, err, "An error occurred during the update", "danger") + return + } else { + delete(s.updateTransactions, utId) + } + } else { + packageHref := "" + updates := make([]map[string]any, 0) + updateGetters := make([]update.PackagesGetter, 0, 1) + if pkgName != "" { + packageHref = "/clusterpackages/" + pkgName + // update concerns cluster packages + if pkgName == "-" { + // prepare updates for all installed packages + updateGetters = append(updateGetters, update.GetAllClusterPackages()) + } else { + // prepare update for a specific package + updateGetters = append(updateGetters, update.GetClusterPackageWithName(pkgName)) + } + } else { + // update concerns namespaced packages + packageHref = util.GetNamespacedPkgHref(manifestName, namespace, name) + if manifestName == "-" { + // prepare updates for all installed namespaced packages + updateGetters = append(updateGetters, update.GetAllPackages("")) + } else { + // prepare update for a specific namespaced package + updateGetters = append(updateGetters, update.GetPackageWithName(namespace, name)) + } + } - for _, u := range ut.Items { - if u.UpdateRequired() { + updater := update.NewUpdater(ctx).WithStatusWriter(statuswriter.Stderr()) + updateTx, err := updater.Prepare(ctx, updateGetters...) + if err != nil { + s.respondAlertAndLog(w, err, "An error occurred preparing update of "+pkgName, "danger") + return + } + utId := rand.Int() + s.updateMutex.Lock() + s.updateTransactions[utId] = *updateTx + s.updateMutex.Unlock() + + for _, u := range updateTx.Items { + if u.UpdateRequired() { + updates = append(updates, map[string]any{ + "Package": u.Package, + "CurrentVersion": u.Package.GetSpec().PackageInfo.Version, + "LatestVersion": u.Version, + }) + } + } + for _, req := range updateTx.Requirements { updates = append(updates, map[string]any{ - "Name": u.Package.Name, - "CurrentVersion": u.Package.Spec.PackageInfo.Version, - "LatestVersion": u.Version, + "Package": req, + "CurrentVersion": "-", + "LatestVersion": req.Version, }) } - } - for _, req := range ut.Requirements { - updates = append(updates, map[string]any{ - "Name": req.Name, - "CurrentVersion": "-", - "LatestVersion": req.Version, + + err = s.templates.pkgUpdateModalTmpl.Execute(w, map[string]any{ + "UpdateTransactionId": utId, + "Updates": updates, + "PackageHref": packageHref, }) + checkTmplError(err, "pkgUpdateModalTmpl") } - - err = s.templates.pkgUpdateModalTmpl.Execute(w, map[string]any{ - "UpdateTransactionId": utId, - "Updates": updates, - "PackageName": pkgName, - }) - checkTmplError(err, "pkgUpdateModalTmpl") } -func (s *server) update(w http.ResponseWriter, r *http.Request) { +// uninstall is an endpoint, which returns the modal html for GET requests, and performs the uninstallation for POST +func (s *server) uninstall(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - updater := update.NewUpdater(ctx).WithStatusWriter(statuswriter.Stderr()) - s.updateMutex.Lock() - defer s.updateMutex.Unlock() - utIdStr := r.FormValue("updateTransactionId") - if utId, err := strconv.Atoi(utIdStr); err != nil { - s.respondAlertAndLog(w, err, "Failed to parse updateTransactionId", "danger") - return - } else if ut, ok := s.updateTransactions[utId]; !ok { - s.respondAlert(w, fmt.Sprintf("Failed to find UpdateTransaction with ID %d", utId), "danger") - return - } else if _, err := updater.Apply(ctx, &ut); err != nil { - delete(s.updateTransactions, utId) - s.respondAlertAndLog(w, err, "An error occurred during the update", "danger") - return - } else { - delete(s.updateTransactions, utId) - } -} + pkgName := mux.Vars(r)["pkgName"] + manifestName := mux.Vars(r)["manifestName"] + namespace := mux.Vars(r)["namespace"] + name := mux.Vars(r)["name"] -func (s *server) uninstallModal(w http.ResponseWriter, r *http.Request) { - pkgName := r.FormValue("packageName") - var pruned []string - var err error - if g, err1 := s.dependencyMgr.NewGraph(r.Context()); err1 != nil { - err = fmt.Errorf("error validating uninstall: %w", err1) + if r.Method == http.MethodPost { + uninstaller := uninstall.NewUninstaller(s.pkgClient).WithStatusWriter(statuswriter.Stderr()) + if pkgName != "" { + var pkg v1alpha1.ClusterPackage + if err := s.pkgClient.ClusterPackages().Get(ctx, pkgName, &pkg); err != nil { + s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching %v during uninstall", pkgName), "danger") + return + } + if err := uninstaller.Uninstall(ctx, &pkg); err != nil { + s.respondAlertAndLog(w, err, "An error occurred uninstalling "+pkgName, "danger") + return + } + } else { + var pkg v1alpha1.Package + if err := s.pkgClient.Packages(namespace).Get(ctx, name, &pkg); err != nil { + s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching %v during uninstall", name), "danger") + return + } + if err := uninstaller.Uninstall(ctx, &pkg); err != nil { + s.respondAlertAndLog(w, err, "An error occurred uninstalling "+name, "danger") + return + } + } } else { - g.Delete(pkgName) - pruned = g.Prune() - if err1 := g.Validate(); err1 != nil { - err = fmt.Errorf("%v cannot be uninstalled: %w", pkgName, err1) + if pkgName != "" { + var pruned []string + var err error + // dependency checks are only necessary for clusterpackages, as there are no dependencies on namespaced packages + if g, err1 := s.dependencyMgr.NewGraph(r.Context()); err1 != nil { + err = fmt.Errorf("error validating uninstall: %w", err1) + } else { + g.Delete(pkgName) + pruned = g.Prune() + if err1 := g.Validate(); err1 != nil { + err = fmt.Errorf("%v cannot be uninstalled: %w", pkgName, err1) + } + } + err = s.templates.pkgUninstallModalTmpl.Execute(w, map[string]any{ + "PackageName": pkgName, + "Pruned": pruned, + "Err": err, + "PackageHref": util.GetClusterPkgHref(pkgName), + }) + checkTmplError(err, "pkgUninstallModalTmpl") + } else { + err := s.templates.pkgUninstallModalTmpl.Execute(w, map[string]any{ + "Namespace": namespace, + "Name": name, + "PackageHref": util.GetNamespacedPkgHref(manifestName, namespace, name), + }) + checkTmplError(err, "pkgUninstallModalTmpl") } } - err = s.templates.pkgUninstallModalTmpl.Execute(w, map[string]any{ - "PackageName": pkgName, - "Pruned": pruned, - "Err": err, - }) - checkTmplError(err, "pkgUninstallModalTmpl") } -func (s *server) uninstall(w http.ResponseWriter, r *http.Request) { +func (s *server) open(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - pkgName := r.FormValue("packageName") - var pkg v1alpha1.ClusterPackage - if err := s.pkgClient.ClusterPackages().Get(ctx, pkgName, &pkg); err != nil { - s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching %v during uninstall", pkgName), "danger") - return - } - if err := uninstall.NewUninstaller(s.pkgClient). - WithStatusWriter(statuswriter.Stderr()). - Uninstall(ctx, &pkg); err != nil { - s.respondAlertAndLog(w, err, "An error occurred uninstalling "+pkgName, "danger") - return + pkgName := mux.Vars(r)["pkgName"] + namespace := mux.Vars(r)["namespace"] + name := mux.Vars(r)["name"] + + if pkgName != "" { + var pkg v1alpha1.ClusterPackage + if err := s.pkgClient.ClusterPackages().Get(ctx, pkgName, &pkg); err != nil { + s.respondAlertAndLog(w, err, "Could not get ClusterPackage", "danger") + return + } + s.handleOpen(ctx, w, &pkg) + } else { + var pkg v1alpha1.Package + if err := s.pkgClient.Packages(namespace).Get(ctx, name, &pkg); err != nil { + s.respondAlertAndLog(w, err, "Could not get Package", "danger") + return + } + s.handleOpen(ctx, w, &pkg) } } -func (s *server) open(w http.ResponseWriter, r *http.Request) { - pkgName := r.FormValue("packageName") - if result, ok := s.forwarders[pkgName]; ok { +func (s *server) handleOpen(ctx context.Context, w http.ResponseWriter, pkg ctrlpkg.Package) { + fwName := cache.NewObjectName(pkg.GetNamespace(), pkg.GetName()).String() + if result, ok := s.forwarders[fwName]; ok { result.WaitReady() _ = cliutils.OpenInBrowser(result.Url) return } - var pkg v1alpha1.ClusterPackage - if err := s.pkgClient.ClusterPackages().Get(r.Context(), pkgName, &pkg); err != nil { - s.respondAlertAndLog(w, err, "Could not get ClusterPackage", "danger") - return - } - - result, err := open.NewOpener().Open(r.Context(), &pkg, "", 0) + result, err := open.NewOpener().Open(ctx, pkg, "", 0) if err != nil { - s.respondAlertAndLog(w, err, "Could not open "+pkgName, "danger") + s.respondAlertAndLog(w, err, "Could not open "+pkg.GetName(), "danger") } else { - s.forwarders[pkgName] = result + s.forwarders[fwName] = result result.WaitReady() _ = cliutils.OpenInBrowser(result.Url) w.WriteHeader(http.StatusAccepted) } } +func (s *server) clusterPackages(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + clpkgs, listErr := list.NewLister(ctx).GetClusterPackagesWithStatus(ctx, list.ListOptions{IncludePackageInfos: true}) + if listErr != nil && len(clpkgs) == 0 { + listErr = fmt.Errorf("could not load clusterpackages: %w", listErr) + fmt.Fprintf(os.Stderr, "%v\n", listErr) + } + + // Call isUpdateAvailable for each installed clusterpackage. + // This is not the same as getting all updates in a single transaction, because some dependency + // conflicts could be resolvable by installing individual clpkgs. + installedClpkgs := make([]ctrlpkg.Package, 0, len(clpkgs)) + clpkgUpdateAvailable := map[string]bool{} + for _, pkg := range clpkgs { + if pkg.ClusterPackage != nil { + installedClpkgs = append(installedClpkgs, pkg.ClusterPackage) + } + clpkgUpdateAvailable[pkg.Name] = s.isUpdateAvailableForPkg(r.Context(), pkg.ClusterPackage) + } + + overallUpdatesAvailable := false + if len(installedClpkgs) > 0 { + overallUpdatesAvailable = s.isUpdateAvailable(r.Context(), installedClpkgs) + } + + tmplErr := s.templates.clusterPkgsPageTemplate.Execute(w, s.enrichPage(r, map[string]any{ + "ClusterPackages": clpkgs, + "ClusterPackageUpdateAvailable": clpkgUpdateAvailable, + "UpdatesAvailable": overallUpdatesAvailable, + "PackageHref": util.GetClusterPkgHref("-"), + }, listErr)) + checkTmplError(tmplErr, "clusterpackages") +} + func (s *server) packages(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - packages, listErr := list.NewLister(ctx).GetPackagesWithStatus(ctx, list.ListOptions{IncludePackageInfos: true}) - if listErr != nil && len(packages) == 0 { + allPkgs, listErr := list.NewLister(ctx).GetPackagesWithStatus(ctx, list.ListOptions{IncludePackageInfos: true}) + if listErr != nil { listErr = fmt.Errorf("could not load packages: %w", listErr) fmt.Fprintf(os.Stderr, "%v\n", listErr) + // TODO check again } - // Call isUpdateAvailable for each installed package. - // This is not the same as getting all updates in a single transaction, because some dependency - // conflicts could be resolvable by installing individual packages. packageUpdateAvailable := map[string]bool{} - for _, pkg := range packages { - packageUpdateAvailable[pkg.Name] = pkg.Package != nil && s.isUpdateAvailable(r.Context(), pkg.Name) + var installed []*list.PackagesWithStatus + var available []*list.PackagesWithStatus + var installedPkgs []ctrlpkg.Package + for _, pkgsWithStatus := range allPkgs { + if len(pkgsWithStatus.Packages) > 0 { + for _, pkgWithStatus := range pkgsWithStatus.Packages { + installedPkgs = append(installedPkgs, pkgWithStatus.Package) + + // Call isUpdateAvailable for each installed package. + // This is not the same as getting all updates in a single transaction, because some dependency + // conflicts could be resolvable by installing individual packages. + packageUpdateAvailable[cache.MetaObjectToName(pkgWithStatus.Package).String()] = + s.isUpdateAvailableForPkg(ctx, pkgWithStatus.Package) + } + installed = append(installed, pkgsWithStatus) + } else { + available = append(available, pkgsWithStatus) + } + } + + overallUpdatesAvailable := false + if len(installedPkgs) > 0 { + overallUpdatesAvailable = s.isUpdateAvailable(r.Context(), installedPkgs) } tmplErr := s.templates.pkgsPageTmpl.Execute(w, s.enrichPage(r, map[string]any{ - "Packages": packages, + "InstalledPackages": installed, + "AvailablePackages": available, "PackageUpdateAvailable": packageUpdateAvailable, - "UpdatesAvailable": s.isUpdateAvailable(r.Context()), + "UpdatesAvailable": overallUpdatesAvailable, + "PackageHref": util.GetNamespacedPkgHref("-", "-", "-"), }, listErr)) checkTmplError(tmplErr, "packages") } -func (s *server) packageDetail(w http.ResponseWriter, r *http.Request) { +// installOrConfigurePackage is like installOrConfigureClusterPackage but for packages +func (s *server) installOrConfigurePackage(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - pkgName := mux.Vars(r)["pkgName"] + manifestName := mux.Vars(r)["manifestName"] + namespace := mux.Vars(r)["namespace"] + name := mux.Vars(r)["name"] + requestedNamespace := r.FormValue("requestedNamespace") + requestedName := r.FormValue("requestedName") repositoryName := r.FormValue("repositoryName") selectedVersion := r.FormValue("selectedVersion") + enableAutoUpdate := r.FormValue("enableAutoUpdate") - pkg, manifest, err := describe.DescribeInstalledPackage(ctx, pkgName) - if err != nil && !apierrors.IsNotFound(err) { - s.respondAlertAndLog(w, err, - fmt.Sprintf("An error occurred fetching package details of installed package %v", pkgName), - "danger") + var err error + pkg := &v1alpha1.Package{} + var mf *v1alpha1.PackageManifest + if err := s.pkgClient.Packages(namespace).Get(ctx, name, pkg); err != nil && !apierrors.IsNotFound(err) { + s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching package details of %v", name), "danger") return - } else if pkg != nil { - repositoryName = pkg.Spec.PackageInfo.RepositoryName - } - - var repos []v1alpha1.PackageRepository - if repos, err = s.repoClientset.Meta().GetReposForPackage(pkgName); err != nil { - fmt.Fprintf(os.Stderr, "error getting repos for package; %v", err) - } else if repositoryName == "" { - if len(repos) == 0 { - s.respondAlertAndLog(w, fmt.Errorf("%v not found in any repository", pkgName), "", "danger") - return - } - for _, r := range repos { - repositoryName = r.Name - if r.IsDefaultRepository() { - break - } - } + } else if err != nil { + pkg = nil } - var usedRepo v1alpha1.PackageRepository - if err := s.pkgClient.PackageRepositories().Get(r.Context(), repositoryName, &usedRepo); err != nil { - s.respondAlertAndLog(w, err, - fmt.Sprintf("An error occurred fetching repository %v", repositoryName), - "danger") + repositoryName, mf, err = s.getUsedRepoAndManifest(ctx, pkg, repositoryName, manifestName, selectedVersion) + if err != nil { + s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred getting manifest and repo for %s", manifestName), "danger") return } - var idx repo.PackageIndex - if err := s.repoClientset.ForRepoWithName(repositoryName).FetchPackageIndex(pkgName, &idx); err != nil { - s.respondAlertAndLog(w, err, - fmt.Sprintf("An error occurred fetching package index of %v in repository %v", pkgName, repositoryName), - "danger") + if values, err := extractValues(r, mf); err != nil { + s.respondAlertAndLog(w, err, "An error occurred parsing the form", "danger") return - } - latestVersion := idx.LatestVersion - - if selectedVersion == "" { - selectedVersion = latestVersion - } else if !slices.ContainsFunc(idx.Versions, func(item types.PackageIndexItem) bool { - return item.Version == selectedVersion - }) { - selectedVersion = latestVersion - } - - if manifest == nil { - manifest = &v1alpha1.PackageManifest{} - if err := s.repoClientset.ForRepoWithName(repositoryName). - FetchPackageManifest(pkgName, selectedVersion, manifest); err != nil { - s.respondAlertAndLog(w, err, - fmt.Sprintf("An error occurred fetching manifest of %v in version %v in repository %v", - pkgName, selectedVersion, repositoryName), - "danger") + } else if pkg == nil { + pkg = client.PackageBuilder(manifestName).WithVersion(selectedVersion). + WithVersion(selectedVersion). + WithRepositoryName(repositoryName). + WithAutoUpdates(strings.ToLower(enableAutoUpdate) == "on"). + WithValues(values). + WithNamespace(requestedNamespace). + WithName(requestedName). + BuildPackage() + opts := metav1.CreateOptions{} + err := install.NewInstaller(s.pkgClient). + WithStatusWriter(statuswriter.Stderr()). + Install(ctx, pkg, opts) + if err != nil { + s.respondAlertAndLog(w, err, "An error occurred installing "+manifestName, "danger") + } else { + s.swappingRedirect(w, "/packages", "main", "main") + w.WriteHeader(http.StatusAccepted) + } + } else { + pkg.Spec.Values = values + if err := s.pkgClient.Packages(pkg.GetNamespace()).Update(ctx, pkg); err != nil { + s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred updating package %v", manifestName), "danger") return } - } - - res, err := s.dependencyMgr.Validate(r.Context(), manifest, selectedVersion) - if err != nil { - s.respondAlertAndLog(w, err, - fmt.Sprintf("An error occurred validating dependencies of %v in version %v", pkgName, selectedVersion), - "danger") - return - } - - valueErrors := make(map[string]error) - datalistOptions := make(map[string]*pkg_config_input.PkgConfigInputDatalistOptions) - if pkg != nil { - nsOptions, _ := s.getNamespaceOptions() - pkgsOptions, _ := s.getPackagesOptions(r.Context()) - for key, v := range pkg.Spec.Values { - if _, err := s.valueResolver.ResolveValue(r.Context(), v); err != nil { - valueErrors[key] = util.GetRootCause(err) - } - if v.ValueFrom != nil { - options, err := s.getDatalistOptions(r.Context(), v.ValueFrom, nsOptions, pkgsOptions) - if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - } - datalistOptions[key] = options - } + if _, err := s.valueResolver.Resolve(ctx, values); err != nil { + s.respondAlertAndLog(w, err, "Some values could not be resolved: ", "warning") + } else { + s.respondSuccess(w) } } - - err = s.templates.pkgPageTmpl.Execute(w, s.enrichPage(r, map[string]any{ - "Package": pkg, - "Status": client.GetStatusOrPending(pkg), - "Manifest": manifest, - "LatestVersion": latestVersion, - "UpdateAvailable": pkg != nil && s.isUpdateAvailable(r.Context(), pkgName), - "AutoUpdate": clientutils.AutoUpdateString(pkg, "Disabled"), - "ValidationResult": res, - "ShowConflicts": res.Status == dependency.ValidationResultStatusConflict, - "SelectedVersion": selectedVersion, - "PackageIndex": &idx, - "Repositories": repos, - "RepositoryName": repositoryName, - "ShowConfiguration": (pkg != nil && len(manifest.ValueDefinitions) > 0 && pkg.DeletionTimestamp.IsZero()) || pkg == nil, - "ValueErrors": valueErrors, - "DatalistOptions": datalistOptions, - "ShowDiscussionLink": usedRepo.IsGlasskubeRepo(), - }, err)) - checkTmplError(err, fmt.Sprintf("package-detail (%s)", pkgName)) } -// installOrConfigurePackage is an endpoint which takes POST requests, containing all necessary parameters to either +// installOrConfigureClusterPackage is an endpoint which takes POST requests, containing all necessary parameters to either // install a new package if it does not exist yet, or update the configuration of an existing package. // The name of the concerned package is given in the pkgName query parameter. // In case the given package is not installed yet in the cluster, there must be a form parameter selectedVersion // containing which version should be installed. // In either case, the parameters from the form are parsed and converted into ValueConfiguration objects, which are // being set in the packages spec. -func (s *server) installOrConfigurePackage(w http.ResponseWriter, r *http.Request) { +func (s *server) installOrConfigureClusterPackage(w http.ResponseWriter, r *http.Request) { ctx := r.Context() pkgName := mux.Vars(r)["pkgName"] repositoryName := r.FormValue("repositoryName") selectedVersion := r.FormValue("selectedVersion") enableAutoUpdate := r.FormValue("enableAutoUpdate") + var err error pkg := &v1alpha1.ClusterPackage{} - var mf v1alpha1.PackageManifest - if err := s.pkgClient.ClusterPackages().Get(ctx, pkgName, pkg); err != nil && !apierrors.IsNotFound(err) { + var mf *v1alpha1.PackageManifest + if err = s.pkgClient.ClusterPackages().Get(ctx, pkgName, pkg); err != nil && !apierrors.IsNotFound(err) { s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching package details of %v", pkgName), "danger") return } else if err != nil { pkg = nil } - if pkg == nil { - var repoClient repoclient.RepoClient - if len(repositoryName) == 0 { - repos, err := s.repoClientset.Meta().GetReposForPackage(pkgName) - if err != nil { - s.respondAlertAndLog(w, err, "", "danger") - return - } - switch len(repos) { - case 0: - // TODO: show error in UI - fmt.Fprintf(os.Stderr, "package not found in any repository") - return - case 1: - repositoryName = repos[0].Name - repoClient = s.repoClientset.ForRepo(repos[0]) - default: - // TODO: show error in UI - fmt.Fprintf(os.Stderr, "package found in multiple repositories") - return - } - } else { - repoClient = s.repoClientset.ForRepoWithName(repositoryName) - } - if err := repoClient.FetchPackageManifest(pkgName, selectedVersion, &mf); err != nil { - s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching manifest of %v in version %v", pkgName, selectedVersion), "danger") - return - } - } else { - if mf1, err := manifest.GetInstalledManifestForPackage(ctx, pkg); err != nil { - s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching package details of %v", pkgName), "danger") - return - } else { - mf = *mf1 - } + repositoryName, mf, err = s.getUsedRepoAndManifest(ctx, pkg, repositoryName, pkgName, selectedVersion) + if err != nil { + s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred getting manifest and repo for %s", pkgName), "danger") + return } - if values, err := extractValues(r, &mf); err != nil { + if values, err := extractValues(r, mf); err != nil { s.respondAlertAndLog(w, err, "An error occurred parsing the form", "danger") return } else if pkg == nil { - pkg = client.ClusterPackageBuilder(pkgName). + pkg = client.PackageBuilder(pkgName).WithVersion(selectedVersion). WithVersion(selectedVersion). WithRepositoryName(repositoryName). WithAutoUpdates(strings.ToLower(enableAutoUpdate) == "on"). WithValues(values). - Build() + BuildClusterPackage() opts := metav1.CreateOptions{} err := install.NewInstaller(s.pkgClient). WithStatusWriter(statuswriter.Stderr()). Install(ctx, pkg, opts) if err != nil { s.respondAlertAndLog(w, err, "An error occurred installing "+pkgName, "danger") - return } } else { pkg.Spec.Values = values @@ -587,26 +652,58 @@ func (s *server) installOrConfigurePackage(w http.ResponseWriter, r *http.Reques if _, err := s.valueResolver.Resolve(ctx, values); err != nil { s.respondAlertAndLog(w, err, "Some values could not be resolved: ", "warning") } else { - err := s.templates.alertTmpl.Execute(w, map[string]any{ - "Message": "Configuration updated successfully", - "Dismissible": true, - "Type": "success", - }) - checkTmplError(err, "success") + s.respondSuccess(w) + } + } +} + +func (s *server) getUsedRepoAndManifest(ctx context.Context, pkg ctrlpkg.Package, repositoryName string, manifestName string, selectedVersion string) ( + string, *v1alpha1.PackageManifest, error) { + + var mf v1alpha1.PackageManifest + if pkg.IsNil() { + var repoClient repoclient.RepoClient + if len(repositoryName) == 0 { + repos, err := s.repoClientset.Meta().GetReposForPackage(manifestName) + if err != nil { + return "", nil, err + } + switch len(repos) { + case 0: + return "", nil, errors.New("package not found in any repository") + case 1: + repositoryName = repos[0].Name + repoClient = s.repoClientset.ForRepo(repos[0]) + default: + return "", nil, errors.New("package found in multiple repositories") + } + } else { + repoClient = s.repoClientset.ForRepoWithName(repositoryName) + } + if err := repoClient.FetchPackageManifest(manifestName, selectedVersion, &mf); err != nil { + return "", nil, err + } + } else { + if installedMf, err := manifest.GetInstalledManifestForPackage(ctx, pkg); err != nil { + return "", nil, err + } else { + mf = *installedMf } } + return repositoryName, &mf, nil } -// advancedConfiguration is a GET+POST endpoint which can be used for advanced package installation options, most notably -// for changing the package repository and changing to a specific (maybe even lower than installed) version of the package. -// It is only intended to be used for already installed packages, for new packages these options exist anyway and -// should be available for every user. -func (s *server) advancedConfiguration(w http.ResponseWriter, r *http.Request) { +// advancedClusterPackageConfiguration is a GET+POST endpoint which can be used for advanced package installation options, +// most notably for changing the package repository and changing to a specific (maybe even lower than installed) +// version of the package. +// It is only intended to be used for already installed clusterpackages, for new clusterpackages these options exist +// anyway and should be available for every user. +func (s *server) advancedClusterPackageConfiguration(w http.ResponseWriter, r *http.Request) { ctx := r.Context() pkgName := mux.Vars(r)["pkgName"] repositoryName := r.FormValue("repositoryName") selectedVersion := r.FormValue("selectedVersion") - pkg, manifest, err := describe.DescribeInstalledPackage(ctx, pkgName) + pkg, manifest, err := describe.DescribeInstalledClusterPackage(ctx, pkgName) if err != nil && !apierrors.IsNotFound(err) { s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching package details of installed package %v", pkgName), @@ -620,16 +717,58 @@ func (s *server) advancedConfiguration(w http.ResponseWriter, r *http.Request) { } else if repositoryName == "" { repositoryName = pkg.Spec.PackageInfo.RepositoryName } + s.handleAdvancedConfig(ctx, &packageDetailPageContext{ + repositoryName: repositoryName, + selectedVersion: selectedVersion, + manifestName: pkgName, + pkg: pkg, + manifest: manifest, + }, r, w) +} + +// advancedPackageConfiguration is like advancedClusterPackageConfiguration but for packages +func (s *server) advancedPackageConfiguration(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + manifestName := mux.Vars(r)["manifestName"] + namespace := mux.Vars(r)["namespace"] + name := mux.Vars(r)["name"] + repositoryName := r.FormValue("repositoryName") + selectedVersion := r.FormValue("selectedVersion") + pkg, manifest, err := describe.DescribeInstalledPackage(ctx, namespace, name) + if err != nil && !apierrors.IsNotFound(err) { + s.respondAlertAndLog(w, err, + fmt.Sprintf("An error occurred fetching package details of installed package %v", manifestName), + "danger") + return + } else if pkg == nil { + s.respondAlertAndLog(w, err, + fmt.Sprintf("Package %v is not installed", manifestName), + "danger") + return + } else if repositoryName == "" { + repositoryName = pkg.Spec.PackageInfo.RepositoryName + } + s.handleAdvancedConfig(ctx, &packageDetailPageContext{ + repositoryName: repositoryName, + selectedVersion: selectedVersion, + manifestName: manifestName, + pkg: pkg, + manifest: manifest, + }, r, w) +} + +func (s *server) handleAdvancedConfig(ctx context.Context, d *packageDetailPageContext, r *http.Request, w http.ResponseWriter) { + var err error var repos []v1alpha1.PackageRepository - if repos, err = s.repoClientset.Meta().GetReposForPackage(pkgName); err != nil { + if repos, err = s.repoClientset.Meta().GetReposForPackage(d.manifestName); err != nil { fmt.Fprintf(os.Stderr, "error getting repos for package; %v", err) - } else if repositoryName == "" { + } else if d.repositoryName == "" { if len(repos) == 0 { - s.respondAlertAndLog(w, fmt.Errorf("%v not found in any repository", pkgName), "", "danger") + s.respondAlertAndLog(w, fmt.Errorf("%v not found in any repository", d.manifestName), "", "danger") return } for _, r := range repos { - repositoryName = r.Name + d.repositoryName = r.Name if r.IsDefaultRepository() { break } @@ -638,59 +777,71 @@ func (s *server) advancedConfiguration(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { var idx repo.PackageIndex - if err := s.repoClientset.ForRepoWithName(repositoryName).FetchPackageIndex(pkgName, &idx); err != nil { + if err := s.repoClientset.ForRepoWithName(d.repositoryName).FetchPackageIndex(d.manifestName, &idx); err != nil { s.respondAlertAndLog(w, err, - fmt.Sprintf("An error occurred fetching package index of %v in repository %v", pkgName, repositoryName), + fmt.Sprintf("An error occurred fetching package index of %v in repository %v", d.manifestName, d.repositoryName), "danger") return } latestVersion := idx.LatestVersion - if selectedVersion == "" { - selectedVersion = latestVersion - } else if !slices.ContainsFunc(idx.Versions, func(item types.PackageIndexItem) bool { - return item.Version == selectedVersion + if d.selectedVersion == "" { + d.selectedVersion = latestVersion + } else if !slices.ContainsFunc(idx.Versions, func(item repotypes.PackageIndexItem) bool { + return item.Version == d.selectedVersion }) { - selectedVersion = latestVersion + d.selectedVersion = latestVersion } - res, err := s.dependencyMgr.Validate(r.Context(), manifest, selectedVersion) + res, err := s.dependencyMgr.Validate(r.Context(), d.manifest, d.selectedVersion) if err != nil { s.respondAlertAndLog(w, err, - fmt.Sprintf("An error occurred validating dependencies of %v in version %v", pkgName, selectedVersion), + fmt.Sprintf("An error occurred validating dependencies of %v in version %v", d.manifestName, d.selectedVersion), "danger") return } err = s.templates.pkgConfigAdvancedTmpl.Execute(w, s.enrichPage(r, map[string]any{ - "Status": client.GetStatusOrPending(pkg), - "Manifest": manifest, + "Status": client.GetStatusOrPending(d.pkg), + "Manifest": d.manifest, "LatestVersion": latestVersion, "ValidationResult": res, "ShowConflicts": res.Status == dependency.ValidationResultStatusConflict, - "SelectedVersion": selectedVersion, + "SelectedVersion": d.selectedVersion, "PackageIndex": &idx, "Repositories": repos, - "RepositoryName": repositoryName, + "RepositoryName": d.repositoryName, + "SelfHref": fmt.Sprintf("%s/configure/advanced", util.GetPackageHref(d.pkg, d.manifest)), }, err)) - checkTmplError(err, fmt.Sprintf("advanced-config (%s)", pkgName)) + checkTmplError(err, fmt.Sprintf("advanced-config (%s)", d.manifestName)) } else if r.Method == http.MethodPost { - pkg.Spec.PackageInfo.Version = selectedVersion - if repositoryName != "" { - pkg.Spec.PackageInfo.RepositoryName = repositoryName + d.pkg.GetSpec().PackageInfo.Version = d.selectedVersion + if d.repositoryName != "" { + d.pkg.GetSpec().PackageInfo.RepositoryName = d.repositoryName } - if err := s.pkgClient.ClusterPackages().Update(ctx, pkg); err != nil { - s.respondAlertAndLog(w, err, - fmt.Sprintf("An error occurred updating package %v to version %v in repo %v", pkgName, selectedVersion, repositoryName), - "danger") - return - } else { - err := s.templates.alertTmpl.Execute(w, map[string]any{ - "Message": "Configuration updated successfully", - "Dismissible": true, - "Type": "success", - }) - checkTmplError(err, "success") + switch pkg := d.pkg.(type) { + case *v1alpha1.ClusterPackage: + if err := s.pkgClient.ClusterPackages().Update(ctx, pkg); err != nil { + s.respondAlertAndLog(w, err, + fmt.Sprintf("An error occurred updating clusterpackage %v to version %v in repo %v", + d.manifestName, d.selectedVersion, d.repositoryName), + "danger") + return + } else { + s.respondSuccess(w) + } + case *v1alpha1.Package: + if err := s.pkgClient.Packages(d.pkg.GetNamespace()).Update(ctx, pkg); err != nil { + s.respondAlertAndLog(w, err, + fmt.Sprintf("An error occurred updating package %v to version %v in repo %v", + d.manifestName, d.selectedVersion, d.repositoryName), + "danger") + return + } else { + s.respondSuccess(w) + } + default: + panic("unexpected package type") } } } @@ -936,13 +1087,15 @@ func (server *server) initWhenBootstrapped(ctx context.Context) { } func (server *server) initCachedClient(ctx context.Context) { + clusterPackageStore, clusterPackageController := server.initClusterPackageStoreAndController(ctx) packageStore, packageController := server.initPackageStoreAndController(ctx) packageInfoStore, packageInfoController := server.initPackageInfoStoreAndController(ctx) packageRepoStore, packageRepoController := server.initPackageRepoStoreAndController(ctx) + go clusterPackageController.Run(ctx.Done()) go packageController.Run(ctx.Done()) go packageInfoController.Run(ctx.Done()) go packageRepoController.Run(ctx.Done()) - server.pkgClient = server.pkgClient.WithStores(packageStore, packageInfoStore, packageRepoStore) + server.pkgClient = server.pkgClient.WithStores(clusterPackageStore, packageStore, packageInfoStore, packageRepoStore) } func (s *server) enrichContext(h http.Handler) http.Handler { @@ -994,7 +1147,7 @@ func defaultKubeconfigExists() bool { } } -func (s *server) initPackageStoreAndController(ctx context.Context) (cache.Store, cache.Controller) { +func (s *server) initClusterPackageStoreAndController(ctx context.Context) (cache.Store, cache.Controller) { pkgClient := s.pkgClient return cache.NewInformer( &cache.ListWatch{ @@ -1012,32 +1165,86 @@ func (s *server) initPackageStoreAndController(ctx context.Context) (cache.Store cache.ResourceEventHandlerFuncs{ AddFunc: func(obj any) { if pkg, ok := obj.(*v1alpha1.ClusterPackage); ok { - s.broadcastRefreshTriggers(pkg) + s.broadcastClusterPackageRefreshTriggers(pkg) } }, UpdateFunc: func(oldObj, newObj any) { if pkg, ok := newObj.(*v1alpha1.ClusterPackage); ok { - s.broadcastRefreshTriggers(pkg) + s.broadcastClusterPackageRefreshTriggers(pkg) } }, DeleteFunc: func(obj any) { if pkg, ok := obj.(*v1alpha1.ClusterPackage); ok { - s.broadcastRefreshTriggers(pkg) + s.broadcastClusterPackageRefreshTriggers(pkg) + fwName := pkg.GetName() + if result, ok := s.forwarders[fwName]; ok { + result.Stop() + delete(s.forwarders, fwName) + } } }, }, ) } -func (s *server) broadcastRefreshTriggers(pkg *v1alpha1.ClusterPackage) { +func (s *server) initPackageStoreAndController(ctx context.Context) (cache.Store, cache.Controller) { + pkgClient := s.pkgClient + return cache.NewInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + var pkgList v1alpha1.PackageList + err := pkgClient.Packages("").GetAll(ctx, &pkgList) + return &pkgList, err + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + return pkgClient.Packages("").Watch(ctx, withDecreasedTimeout(options)) + }, + }, + &v1alpha1.Package{}, + 0, + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj any) { + if pkg, ok := obj.(*v1alpha1.Package); ok { + s.broadcastPackageRefreshTriggers(pkg) + } + }, + UpdateFunc: func(oldObj, newObj any) { + if pkg, ok := newObj.(*v1alpha1.Package); ok { + s.broadcastPackageRefreshTriggers(pkg) + } + }, + DeleteFunc: func(obj any) { + if pkg, ok := obj.(*v1alpha1.Package); ok { + s.broadcastPackageRefreshTriggers(pkg) + fwName := cache.ObjectName{Namespace: pkg.GetNamespace(), Name: pkg.GetName()}.String() + if result, ok := s.forwarders[fwName]; ok { + result.Stop() + delete(s.forwarders, fwName) + } + } + }, + }, + ) +} + +func (s *server) broadcastClusterPackageRefreshTriggers(pkg *v1alpha1.ClusterPackage) { s.sseHub.Broadcast <- &sse{ - event: "refresh-pkg-overview", + event: "refresh-clusterpackage-overview", } s.sseHub.Broadcast <- &sse{ event: fmt.Sprintf("refresh-pkg-detail-%s", pkg.Name), } } +func (s *server) broadcastPackageRefreshTriggers(pkg *v1alpha1.Package) { + s.sseHub.Broadcast <- &sse{ + event: "refresh-package-overview", + } + s.sseHub.Broadcast <- &sse{ + event: fmt.Sprintf("refresh-pkg-detail-%s-%s", pkg.Namespace, pkg.Name), + } +} + func (s *server) initPackageInfoStoreAndController(ctx context.Context) (cache.Store, cache.Controller) { pkgClient := s.pkgClient return cache.NewInformer( @@ -1085,33 +1292,20 @@ func withDecreasedTimeout(opts metav1.ListOptions) metav1.ListOptions { return opts } -func (s *server) isUpdateAvailable(ctx context.Context, packages ...string) bool { - if tx, err := update.NewUpdater(ctx).Prepare(ctx, packages); err != nil { - fmt.Fprintf(os.Stderr, "Error checking for updates: %v\n", err) +func (s *server) isUpdateAvailableForPkg(ctx context.Context, pkg ctrlpkg.Package) bool { + if pkg.IsNil() { return false - } else { - return !tx.IsEmpty() } + return s.isUpdateAvailable(ctx, []ctrlpkg.Package{pkg}) } -func (s *server) respondAlertAndLog(w http.ResponseWriter, err error, wrappingMsg string, alertType string) { - if wrappingMsg != "" { - err = fmt.Errorf("%v: %w", wrappingMsg, err) +func (s *server) isUpdateAvailable(ctx context.Context, pkgs []ctrlpkg.Package) bool { + if tx, err := update.NewUpdater(ctx).Prepare(ctx, update.GetExact(pkgs)); err != nil { + fmt.Fprintf(os.Stderr, "Error checking for updates: %v\n", err) + return false + } else { + return !tx.IsEmpty() } - fmt.Fprintf(os.Stderr, "%v\n", err) - s.respondAlert(w, err.Error(), alertType) -} - -func (s *server) respondAlert(w http.ResponseWriter, message string, alertType string) { - w.Header().Add("Hx-Reselect", "div.alert") // overwrite any existing hx-select (which was a little intransparent sometimes) - w.Header().Add("Hx-Reswap", "afterbegin") - w.WriteHeader(http.StatusBadRequest) - err := s.templates.alertTmpl.Execute(w, map[string]any{ - "Message": message, - "Dismissible": true, - "Type": alertType, - }) - checkTmplError(err, "alert") } func isPortConflictError(err error) bool { diff --git a/internal/web/templates.go b/internal/web/templates.go index 9431e651b..7ad11054d 100644 --- a/internal/web/templates.go +++ b/internal/web/templates.go @@ -31,23 +31,24 @@ import ( ) type templates struct { - templateFuncs template.FuncMap - baseTemplate *template.Template - pkgsPageTmpl *template.Template - pkgPageTmpl *template.Template - pkgDiscussionPageTmpl *template.Template - supportPageTmpl *template.Template - bootstrapPageTmpl *template.Template - kubeconfigPageTmpl *template.Template - settingsPageTmpl *template.Template - pkgUpdateModalTmpl *template.Template - pkgConfigInput *template.Template - pkgConfigAdvancedTmpl *template.Template - pkgUninstallModalTmpl *template.Template - alertTmpl *template.Template - datalistTmpl *template.Template - pkgDiscussionBadgeTmpl *template.Template - repoClientset repoclient.RepoClientset + templateFuncs template.FuncMap + baseTemplate *template.Template + clusterPkgsPageTemplate *template.Template + pkgsPageTmpl *template.Template + pkgPageTmpl *template.Template + pkgDiscussionPageTmpl *template.Template + supportPageTmpl *template.Template + bootstrapPageTmpl *template.Template + kubeconfigPageTmpl *template.Template + settingsPageTmpl *template.Template + pkgUpdateModalTmpl *template.Template + pkgConfigInput *template.Template + pkgConfigAdvancedTmpl *template.Template + pkgUninstallModalTmpl *template.Template + alertTmpl *template.Template + datalistTmpl *template.Template + pkgDiscussionBadgeTmpl *template.Template + repoClientset repoclient.RepoClientset } var ( @@ -77,9 +78,9 @@ func (t *templates) watchTemplates() error { func (t *templates) parseTemplates() { t.templateFuncs = template.FuncMap{ - "ForPkgOverviewBtn": pkg_overview_btn.ForPkgOverviewBtn, - "ForPkgDetailBtns": pkg_detail_btns.ForPkgDetailBtns, - "ForPkgUpdateAlert": pkg_update_alert.ForPkgUpdateAlert, + "ForClPkgOverviewBtn": pkg_overview_btn.ForClPkgOverviewBtn, + "ForPkgDetailBtns": pkg_detail_btns.ForPkgDetailBtns, + "ForPkgUpdateAlert": pkg_update_alert.ForPkgUpdateAlert, "PackageManifestUrl": func(pkg ctrlpkg.Package) string { if !pkg.IsNil() { url, err := t.repoClientset.ForPackage(pkg). @@ -135,11 +136,21 @@ func (t *templates) parseTemplates() { cond := meta.FindStatusCondition(repo.Status.Conditions, string(condition.Ready)) return cond != nil && cond.Status == metav1.ConditionTrue }, + "PackageDetailRefreshId": func(manifest *v1alpha1.PackageManifest, pkg ctrlpkg.Package) string { + var id string + if manifest.Scope.IsCluster() { + id = manifest.Name + } else if !pkg.IsNil() { + id = fmt.Sprintf("%s-%s", pkg.GetNamespace(), pkg.GetName()) + } + return fmt.Sprintf("refresh-pkg-detail-%s", id) + }, } t.baseTemplate = template.Must(template.New("base.html"). Funcs(t.templateFuncs). ParseFS(webFs, path.Join(templatesDir, "layout", "base.html"))) + t.clusterPkgsPageTemplate = t.pageTmpl("clusterpackages.html") t.pkgsPageTmpl = t.pageTmpl("packages.html") t.pkgPageTmpl = t.pageTmpl("package.html") t.pkgDiscussionPageTmpl = t.pageTmpl("discussion.html") diff --git a/internal/web/templates/components/pkg-overview-btn.html b/internal/web/templates/components/clpkg-overview-btn.html similarity index 85% rename from internal/web/templates/components/pkg-overview-btn.html rename to internal/web/templates/components/clpkg-overview-btn.html index 926275d5c..2c71ef932 100644 --- a/internal/web/templates/components/pkg-overview-btn.html +++ b/internal/web/templates/components/clpkg-overview-btn.html @@ -1,8 +1,8 @@ -{{ define "pkg-overview-btn" }} +{{ define "clpkg-overview-btn" }} {{ if eq .Status nil }} Install - {{ else if not .Pkg.DeletionTimestamp.IsZero }} + {{ else if .InDeletion }}
@@ -21,8 +21,9 @@ Installation Failed {{ else if .UpdateAvailable }} + {{ if eq .Err nil }} @@ -11,7 +18,7 @@

Uninstall {{ .PackageName }}

{{ else }}