You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(bootstrap): add bootstrap command for first-time Greenhouse access - #83
Implements cloudctl bootstrap as described in #80, giving operators a single command to bootstrap first-time access to a Greenhouse cluster.
--data=<base64-kubeconfig> — decodes a kubeconfig downloaded from the Greenhouse Web UI (standard kubeconfig YAML, base64-encoded). Supports any auth type the UI provides: OIDC auth-provider, exec-plugin, token, or cert. All fields (certificate-authority-data, namespace, full auth-provider config) are preserved verbatim.
Individual OIDC flags — --greenhouse-server, --greenhouse-org, --greenhouse-idp-issuer-url, --greenhouse-client-id, --greenhouse-client-secret, --greenhouse-extra-scopes, --greenhouse-ca-data, --greenhouse-namespace. Produces the exact same auth-provider/oidc kubeconfig shape as the Greenhouse UI download, scriptable without a browser.
Interactive prompts for context name and --set-current-context on TTY; fully non-interactive when flags are provided.
Context rename: all three entries (cluster, user, context) are renamed atomically to the user-chosen name; namespace is preserved.
Existing unmanaged entries are never overwritten (idempotent).
--dry-run previews without writing.
Next-step hint printed on success: cloudctl sync -n <org>.
Test plan
TestBuildOIDCKubeconfig_* — unit tests for OIDC kubeconfig construction, including exact match against the real Greenhouse shape
TestRenameKubeconfigContext_* — rename of all three entries; multi-context blob only renames current
Adds `cloudctl bootstrap` which merges a Greenhouse kubeconfig into the
user's local kubeconfig so they can reach the Greenhouse API server with
kubectl and run `cloudctl sync`.
Two input modes are supported:
- `--data=<base64-kubeconfig>`: decodes a standard kubeconfig downloaded
from the Greenhouse Web UI; supports any auth type (OIDC auth-provider,
exec-plugin, token, cert) and preserves all fields verbatim.
- Individual OIDC flags: `--greenhouse-server`, `--greenhouse-org`,
`--greenhouse-idp-issuer-url`, `--greenhouse-client-id`,
`--greenhouse-client-secret`, `--greenhouse-extra-scopes`,
`--greenhouse-ca-data`, `--greenhouse-namespace`. Produces the same
auth-provider/oidc kubeconfig shape as the Greenhouse UI download.
Both modes:
- Ask interactively for context name and whether to set it as current
context (skipped when `--context-name` / `--set-current-context` are
given or when not on a TTY).
- Rename the context triple (cluster + user + context) to the chosen name.
- Never overwrite existing unmanaged kubeconfig entries.
- Are idempotent: running twice with the same input is safe.
- Support `--dry-run` to preview without writing.
- Print a next-step hint: `cloudctl sync -n <org>`.
Closes#80
Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
…org hint
- fix misleading doc comment in buildOIDCKubeconfig (client-secret is always
emitted, not omitted when empty)
- fix Added/Skipped JSON/YAML tags from omitempty to omitzero (Go 1.25 idiom)
- add CurrentContextUpdated field to BootstrapResult; printers now show
"nothing new" only when no entries were added AND current-context was not
changed
- capture org from the selected context's namespace before renameKubeconfigContext
so the sync hint is correct when --context-name differs from greenhouse-<org>
- when --kubeconfig is not explicitly set, load through
clientcmd.NewDefaultClientConfigLoadingRules to honour multi-file KUBECONFIG
- make renameKubeconfigContext safe for shared cluster/authinfo references:
only delete old keys if no other context still references them
- validate blob context in resolveIncomingKubeconfig: error when current-context
is missing or ambiguous, or when context references a non-existent cluster/user
- add tests: SharedClusterPreserved rename, DataBlobNoCurrentContext,
DataBlobMissingClusterRef
Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
When KUBECONFIG starts with a path separator (e.g. :/second/config),
the first segment is empty and the write target is ambiguous. Return a
clear error matching the behaviour of resolveWriteTarget in sync.go.
Also reset cobra flag Changed state between test runs so that flag.Changed()
is accurate regardless of test ordering.
Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
… matches
When the incoming context key already equals targetName, the early return
skipped renaming the referenced cluster and authinfo entries. This caused
the three map entries to have inconsistent names after merge.
Separate the "context key is a no-op" case from "entries need renaming"
so cluster and authinfo are always aligned to targetName regardless of
whether the context key itself needed changing.
Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟡 Changes recommended
Context renaming and independent map merging can still overwrite incoming target-name entries or create mixed invalid configurations when names collide.
…t sync hint
Three fixes:
1. When KUBECONFIG spans multiple files, load a merged view for collision
detection but mutate and serialize only the first file. Previously the
merged object was written back to the first file, silently copying
unmanaged entries from other files into it.
2. Interactive and plain printers now always emit the "cloudctl sync -n
<org>" next-step hint after a successful bootstrap, including the
idempotent case. Previously the hint was skipped when nothing was new.
3. "Bootstrap complete." and kubeconfig path are suppressed for the
idempotent case (nothing to report), but the hint still prints.
Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
Create the kubeconfig parent directory before writing
cmd/bootstrap.go:225
On a fresh machine the default parent directory (~/.kube) may not exist, and clientcmd.WriteToFile does not create parent directories. The first-time bootstrap therefore fails before creating the default kubeconfig. Create the destination directory with restrictive permissions before writing, and cover a path whose parent is absent.
Generate a usable sync command hint
cmd/output/interactive_printer.go:161
This advertised next command is not usable when the user declines --set-current-context (the default): sync then reads another or empty current context unless -c is supplied. It also misses -k after bootstrap writes an explicitly selected kubeconfig. Build the hint from ContextName, SetAsCurrent, and KubeconfigPath so it actually targets the bootstrapped access.
Generate a usable sync command hint
cmd/output/plain_printer.go:136
This advertised next command is not usable when the user declines --set-current-context (the default): sync then reads another or empty current context unless -c is supplied. It also misses -k after bootstrap writes an explicitly selected kubeconfig. Build the hint from ContextName, SetAsCurrent, and KubeconfigPath so it actually targets the bootstrapped access.
…load
A malformed or unreadable file listed after the first path in KUBECONFIG
would cause Load() to error, but the previous code silently fell back to
the first-file view. Collision detection then missed entries from the
remaining files and bootstrap could shadow unmanaged configuration.
Return the error instead of continuing with an incomplete view.
Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
A multi-context kubeconfig blob where one context value is null (e.g.
decoded from a YAML null entry) caused a nil pointer dereference in the
cluster and authinfo ref-count loops inside renameKubeconfigContext.
Add a c != nil guard in both loops so the rename proceeds safely.
Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
… value
cmd.Flags().Changed("kubeconfig") only saw the --kubeconfig flag; values
provided via the CLOUDCTL_KUBECONFIG environment variable or a cloudctl
config file were silently ignored, causing bootstrap to fall back to
KUBECONFIG / home default and potentially write to the wrong file.
The kubeconfig write target and the merged-view skip gate now check both
cmd.Flags().Changed("kubeconfig") (flag path) and viper.IsSet("kubeconfig")
(env-var / config-file path). Similarly, the interactive prompts for
--context-name and --set-current-context now respect values from any
configuration source.
Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
Avoid rewriting kubeconfig when no changes were made
cmd/bootstrap.go:235
An idempotent run with every entry skipped and no current-context change still rewrites the kubeconfig here. Besides contradicting the “nothing new to write” result, reserialization changes the file's mtime and can discard comments/formatting in unmanaged configuration. Only write when an entry was added or the current context changed.
renameKubeconfigContext silently overwrote an existing cluster, authinfo,
or context entry when the chosen --context-name matched a key belonging to
a different context in the blob. The function now returns an error before
mutating any of the three maps when targetName already names an unrelated
entry, preventing silent data loss in multi-context blobs.
Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
This writes the kubeconfig even when every entry was skipped and the current context is unchanged. Besides unnecessarily changing the file's mtime, loading and serializing can remove comments/formatting despite the printer saying “nothing new to write.” Skip writeConfig when the result contains no actual change.
Select bootstrapped context in suggested command
cmd/output/interactive_printer.go:161
When SetAsCurrent is false, the suggested command does not select the context that bootstrap just added. It therefore fails for a fresh kubeconfig and can query an unrelated cluster when another context is current. Add --greenhouse-cluster-context <ContextName> to this branch's hint unless the bootstrapped context was selected.
Include context in hint when it is not set current
cmd/output/plain_printer.go:136
When the user declines --set-current-context (the default in non-interactive mode), this suggested command uses whatever context was previously current; on a new kubeconfig there is no current context, so the advertised next step fails. Include --greenhouse-cluster-context <ContextName> whenever SetAsCurrent is false.
Reading dry-run only via cmd.Flags().GetBool ignored CLOUDCTL_DRY_RUN
and a dry-run key in the cloudctl config file, contrary to the
configuration contract. The flag is now checked first (to avoid viper
cross-command pollution with sync's dry-run binding), with a fallback
to viper.IsSet so env-var and config-file sources are respected.
Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
Only the active context is validated, but mergeBootstrapKubeconfig subsequently copies every context, cluster, and auth-info from the blob. A multi-context blob can therefore contain a valid active context plus a dangling or null non-active context and have that invalid entry written into the user's kubeconfig. Either validate every entry that will be merged (including non-nil referenced objects) or reduce the incoming config to the selected access tuple before merging.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements
cloudctl bootstrapas described in #80, giving operators a single command to bootstrap first-time access to a Greenhouse cluster.--data=<base64-kubeconfig>— decodes a kubeconfig downloaded from the Greenhouse Web UI (standard kubeconfig YAML, base64-encoded). Supports any auth type the UI provides: OIDCauth-provider, exec-plugin, token, or cert. All fields (certificate-authority-data,namespace, fullauth-providerconfig) are preserved verbatim.--greenhouse-server,--greenhouse-org,--greenhouse-idp-issuer-url,--greenhouse-client-id,--greenhouse-client-secret,--greenhouse-extra-scopes,--greenhouse-ca-data,--greenhouse-namespace. Produces the exact sameauth-provider/oidckubeconfig shape as the Greenhouse UI download, scriptable without a browser.--set-current-contexton TTY; fully non-interactive when flags are provided.--dry-runpreviews without writing.cloudctl sync -n <org>.Test plan
TestBuildOIDCKubeconfig_*— unit tests for OIDC kubeconfig construction, including exact match against the real Greenhouse shapeTestRenameKubeconfigContext_*— rename of all three entries; multi-context blob only renames currentTestMergeBootstrapKubeconfig_*— add/skip/no-overwrite/preserve behaviourTestResolveIncomingKubeconfig_*— both input modes, all missing-flag error pathsTestBootstrapCmd_*— 11 end-to-end cobra command tests: blob write, individual flags write, parity between modes, dry-run file unchanged, idempotency, context rename, preserves existing entries, creates file from scratch, JSON output, missing flags error, raw base64Closes #80