From ad2f1715468305047e387c6eef5f28cc938c1058 Mon Sep 17 00:00:00 2001 From: Marc Campbell Date: Thu, 11 Dec 2025 17:12:41 -0600 Subject: [PATCH] list hostnames --- cli/cmd/app_hostname.go | 24 +++ cli/cmd/app_hostname_ls.go | 282 ++++++++++++++++++++++++++++++ cli/cmd/root.go | 3 + pkg/kotsclient/custom_hostname.go | 11 ++ pkg/types/custom_hostname.go | 13 ++ 5 files changed, 333 insertions(+) create mode 100644 cli/cmd/app_hostname.go create mode 100644 cli/cmd/app_hostname_ls.go diff --git a/cli/cmd/app_hostname.go b/cli/cmd/app_hostname.go new file mode 100644 index 000000000..02f8fe2f9 --- /dev/null +++ b/cli/cmd/app_hostname.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func (r *runners) InitAppHostnameCommand(parent *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "hostname", + Short: "Manage custom hostnames for applications", + Long: `The hostname command allows you to manage custom hostnames for your application. + +This command provides subcommands for listing and viewing custom hostname configurations +including registry, proxy, download portal, and replicated app hostnames.`, + Example: `# List all custom hostnames for an app +replicated app hostname ls --app myapp + +# List hostnames and output as JSON +replicated app hostname ls --app myapp --output json`, + } + parent.AddCommand(cmd) + + return cmd +} diff --git a/cli/cmd/app_hostname_ls.go b/cli/cmd/app_hostname_ls.go new file mode 100644 index 000000000..8404923bd --- /dev/null +++ b/cli/cmd/app_hostname_ls.go @@ -0,0 +1,282 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "text/tabwriter" + + "github.com/pkg/errors" + "github.com/replicatedhq/replicated/pkg/logger" + "github.com/replicatedhq/replicated/pkg/tools" + "github.com/replicatedhq/replicated/pkg/types" + "github.com/spf13/cobra" +) + +func (r *runners) InitAppHostnameListCommand(parent *cobra.Command) *cobra.Command { + var outputFormat string + + cmd := &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Short: "List custom hostnames for an application", + Long: `List all custom hostnames configured for an application. + +This command fetches and displays all custom hostname configurations including: +- Registry hostnames +- Proxy hostnames +- Download Portal hostnames +- Replicated App hostnames + +The app ID or slug can be provided via the --app flag or from the .replicated config file.`, + Example: `# List all custom hostnames for an app +replicated app hostname ls --app myapp + +# List hostnames and output as JSON +replicated app hostname ls --app myapp --output json`, + PreRunE: func(cmd *cobra.Command, args []string) error { + // The parent chain is: rootCmd -> appCmd -> appHostnameCmd -> ls cmd + // We need to call the app command's PersistentPreRunE (which is preRunSetupAPIs) + // cmd.Parent() = appHostnameCmd + // cmd.Parent().Parent() = appCmd + hostnameCmd := cmd.Parent() + if hostnameCmd != nil { + appCmd := hostnameCmd.Parent() + if appCmd != nil && appCmd.PersistentPreRunE != nil { + if err := appCmd.PersistentPreRunE(cmd, args); err != nil { + return err + } + } + } + + // Load app from .replicated config if not provided via --app flag + if r.appSlug == "" && r.appID == "" { + parser := tools.NewConfigParser() + config, err := parser.FindAndParseConfig(".") + if err == nil && (config.AppSlug != "" || config.AppId != "") { + if config.AppSlug != "" { + r.appSlug = config.AppSlug + } else if config.AppId != "" { + r.appID = config.AppId + } + } + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + return r.listAppHostnames(ctx, outputFormat) + }, + SilenceUsage: true, + } + parent.AddCommand(cmd) + + cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + + return cmd +} + +func (r *runners) listAppHostnames(ctx context.Context, outputFormat string) error { + // Only show spinners for table output + showSpinners := outputFormat == "table" + log := logger.NewLogger(r.w) + + // Resolve app ID from slug or ID + appSlugOrID := r.appSlug + if appSlugOrID == "" { + appSlugOrID = r.appID + } + if appSlugOrID == "" { + return errors.New("app ID or slug is required (use --app flag or set in .replicated config)") + } + + if showSpinners { + log.ActionWithSpinner("Fetching app") + } + app, err := r.kotsAPI.GetApp(ctx, appSlugOrID, true) + if err != nil { + if showSpinners { + log.FinishSpinnerWithError() + } + return errors.Wrap(err, "get app") + } + if showSpinners { + log.FinishSpinner() + } + + // Fetch default hostnames + if showSpinners { + log.ActionWithSpinner("Fetching default hostnames") + } + defaultHostnames, err := r.kotsAPI.ListDefaultHostnames(app.ID) + if err != nil { + if showSpinners { + log.FinishSpinnerWithError() + } + return errors.Wrap(err, "list default hostnames") + } + if showSpinners { + log.FinishSpinner() + } + + // Fetch custom hostnames + if showSpinners { + log.ActionWithSpinner("Fetching custom hostnames") + } + customHostnames, err := r.kotsAPI.ListCustomHostnames(app.ID) + if err != nil { + if showSpinners { + log.FinishSpinnerWithError() + } + return errors.Wrap(err, "list custom hostnames") + } + if showSpinners { + log.FinishSpinner() + } + + // Merge hostnames: start with defaults, override with custom values + mergedHostnames := mergeHostnames(defaultHostnames, customHostnames) + + // Extract just the hostname strings from the merged result + hostnameStrings := extractHostnameStrings(mergedHostnames) + + // Output based on format + if outputFormat == "json" { + jsonBytes, err := json.MarshalIndent(hostnameStrings, "", " ") + if err != nil { + return errors.Wrap(err, "marshal json") + } + // Print directly without log prefix + r.w.Write(jsonBytes) + r.w.Write([]byte("\n")) + r.w.Flush() + return nil + } + + if outputFormat == "table" { + return printHostnamesTable(r.w, hostnameStrings) + } + + return errors.Errorf("unsupported output format: %s", outputFormat) +} + +// extractHostnameStrings extracts just the hostname strings from the merged hostnames +func extractHostnameStrings(merged *types.KotsAppCustomHostnames) map[string]string { + result := make(map[string]string) + + // Take the first (default) hostname from each category + if len(merged.Registry) > 0 { + result["registry"] = merged.Registry[0].Hostname + } + + if len(merged.Proxy) > 0 { + result["proxy"] = merged.Proxy[0].Hostname + } + + if len(merged.DownloadPortal) > 0 { + result["downloadPortal"] = merged.DownloadPortal[0].Hostname + } + + if len(merged.ReplicatedApp) > 0 { + result["replicatedApp"] = merged.ReplicatedApp[0].Hostname + } + + return result +} + +// printHostnamesTable prints hostnames in a table format +func printHostnamesTable(w *tabwriter.Writer, hostnames map[string]string) error { + fmt.Fprintln(w, "TYPE\tHOSTNAME") + + if registry, ok := hostnames["registry"]; ok && registry != "" { + fmt.Fprintf(w, "Registry\t%s\n", registry) + } + + if proxy, ok := hostnames["proxy"]; ok && proxy != "" { + fmt.Fprintf(w, "Proxy\t%s\n", proxy) + } + + if downloadPortal, ok := hostnames["downloadPortal"]; ok && downloadPortal != "" { + fmt.Fprintf(w, "Download Portal\t%s\n", downloadPortal) + } + + if replicatedApp, ok := hostnames["replicatedApp"]; ok && replicatedApp != "" { + fmt.Fprintf(w, "Replicated App\t%s\n", replicatedApp) + } + + w.Flush() + return nil +} + +// mergeHostnames merges default and custom hostnames. +// Defaults are simple strings, custom hostnames are arrays of detailed objects. +// For each category, if custom hostnames exist, use them; otherwise create a basic hostname from the default string. +func mergeHostnames(defaults *types.DefaultHostnames, custom *types.KotsAppCustomHostnames) *types.KotsAppCustomHostnames { + if custom == nil && defaults == nil { + return &types.KotsAppCustomHostnames{} + } + + if custom == nil { + custom = &types.KotsAppCustomHostnames{} + } + + if defaults == nil { + return custom + } + + result := &types.KotsAppCustomHostnames{ + Registry: mergeHostnameList(defaults.Registry, custom.Registry), + Proxy: mergeHostnameList(defaults.Proxy, custom.Proxy), + DownloadPortal: mergeHostnameList(defaults.DownloadPortal, custom.DownloadPortal), + ReplicatedApp: mergeHostnameList(defaults.ReplicatedApp, custom.ReplicatedApp), + } + + return result +} + +// mergeHostnameList merges a default hostname string with custom hostname objects. +// If custom hostnames exist and contain the default hostname, use the custom data. +// If custom hostnames exist but don't contain the default, add both. +// If no custom hostnames, create a basic entry from the default string. +func mergeHostnameList(defaultHostname string, custom []types.KotsAppCustomHostname) []types.KotsAppCustomHostname { + // If there's no default hostname, just return custom + if defaultHostname == "" { + return custom + } + + // If there are no custom hostnames, create a basic one from the default + if len(custom) == 0 { + return []types.KotsAppCustomHostname{ + { + IsDefault: true, + CustomHostname: types.CustomHostname{ + Hostname: defaultHostname, + }, + }, + } + } + + // Check if any custom hostname matches the default + foundDefault := false + result := make([]types.KotsAppCustomHostname, 0, len(custom)) + for _, ch := range custom { + if ch.Hostname == defaultHostname { + foundDefault = true + // Mark this as the default + ch.IsDefault = true + } + result = append(result, ch) + } + + // If the default hostname wasn't in the custom list, add it + if !foundDefault { + result = append(result, types.KotsAppCustomHostname{ + IsDefault: true, + CustomHostname: types.CustomHostname{ + Hostname: defaultHostname, + }, + }) + } + + return result +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 9fe84457a..06bd2dfa0 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -198,6 +198,9 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i runCmds.InitAppCreate(appCmd) runCmds.InitAppRm(appCmd) + appHostnameCmd := runCmds.InitAppHostnameCommand(appCmd) + runCmds.InitAppHostnameListCommand(appHostnameCmd) + defaultCmd := runCmds.InitDefaultCommand(runCmds.rootCmd) runCmds.InitDefaultShowCommand(defaultCmd) runCmds.InitDefaultSetCommand(defaultCmd) diff --git a/pkg/kotsclient/custom_hostname.go b/pkg/kotsclient/custom_hostname.go index 85eb153ea..cb5deedc8 100644 --- a/pkg/kotsclient/custom_hostname.go +++ b/pkg/kotsclient/custom_hostname.go @@ -19,3 +19,14 @@ func (c *VendorV3Client) ListCustomHostnames(appID string) (*types.KotsAppCustom return &resp, nil } + +func (c *VendorV3Client) ListDefaultHostnames(appID string) (*types.DefaultHostnames, error) { + resp := types.DefaultHostnames{} + path := fmt.Sprintf("/v3/app/%s/default-hostnames", appID) + err := c.DoJSON(context.TODO(), "GET", path, http.StatusOK, nil, &resp) + if err != nil { + return nil, errors.Wrapf(err, "list default hostnames appId %s", appID) + } + + return &resp, nil +} diff --git a/pkg/types/custom_hostname.go b/pkg/types/custom_hostname.go index 7ac6f4f9d..a82f93ae6 100644 --- a/pkg/types/custom_hostname.go +++ b/pkg/types/custom_hostname.go @@ -4,6 +4,7 @@ import "time" // CustomHostname represents a custom hostname in cloudflare for a team type CustomHostname struct { + AppID string `json:"app_id"` TeamID string `json:"team_id"` OriginServer string `json:"origin_server"` Hostname string `json:"hostname"` @@ -13,10 +14,14 @@ type CustomHostname struct { DomainVerificationStatus string `json:"domain_verification_status"` DomainTxtRecordName string `json:"domain_txt_record_name"` DomainTxtRecordValue string `json:"domain_txt_record_value"` + DomainChallenge string `json:"-"` + DomainChallengeResponse string `json:"-"` TLSVerificationType string `json:"tls_verification_type"` TLSVerificationStatus string `json:"tls_verification_status"` TLSTxtRecordName string `json:"tls_txt_record_name"` TLSTxtRecordValue string `json:"tls_txt_record_value"` + TLSHTTPChallenge string `json:"-"` + TLSHTTPBody string `json:"-"` CloudflareCustomHostnameID string `json:"cloudflare_custom_hostname_id"` CloudflareWorkerRouteID string `json:"cloudflare_worker_route_id,omitempty"` VerificationErrors []string `json:"verification_errors"` @@ -38,3 +43,11 @@ type KotsAppCustomHostnames struct { DownloadPortal []KotsAppCustomHostname `json:"downloadPortal"` ReplicatedApp []KotsAppCustomHostname `json:"replicatedApp"` } + +// DefaultHostnames represents the default hostnames for a kots app +type DefaultHostnames struct { + Registry string `json:"registry"` + Proxy string `json:"proxy"` + DownloadPortal string `json:"downloadPortal"` + ReplicatedApp string `json:"replicatedApp"` +}