Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmd/orun/command_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ func runAuthStatus() error {
creds, err := cliauth.LoadSession()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// Headless runs authenticate from the environment, not a stored
// session — reporting "not logged in" with ORUN_TOKEN set sends
// operators chasing a non-existent auth problem (hit live).
if strings.TrimSpace(os.Getenv("ORUN_TOKEN")) != "" {
color := ui.ColorEnabledForWriter(os.Stdout)
fmt.Printf("%s headless: authenticated via ORUN_TOKEN (no stored session)\n", ui.Green(color, "✓"))
if backendURL != "" {
fmt.Printf("Backend URL: %s\n", backendURL)
}
fmt.Println("Resolve a workspace's identity with `orun workspace <ws-id-or-slug>`.")
return nil
}
return fmt.Errorf("not logged in; run `orun auth login` or `orun auth login --device`")
}
return err
Expand Down
6 changes: 6 additions & 0 deletions cmd/orun/command_cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ func cloudSessionToken(ctx context.Context, backendURL string) (string, error) {
}

func errNotLoggedIn() error {
// With ORUN_TOKEN set, "set ORUN_TOKEN" is insulting noise: this path is
// then reached because the operation needs something the token cannot
// provide (e.g. a memberships list — workspace-scoped keys have none).
if strings.TrimSpace(os.Getenv("ORUN_TOKEN")) != "" {
return fmt.Errorf("ORUN_TOKEN is set, but this operation needs a stored session or a membership listing the token does not have (workspace-scoped keys list no memberships — resolve identity with `orun workspace <ws-id-or-slug>` instead)")
}
return fmt.Errorf("not logged in to Orun Cloud; run `orun auth login` (or set ORUN_TOKEN for headless runs)")
}

Expand Down
7 changes: 6 additions & 1 deletion cmd/orun/command_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,12 @@ func renderSecretsWriteError(err error, key string) error {
case apiErr.IsLocked():
return fmt.Errorf("cannot write %s: %w\nhint: the key is locked at a higher rung (or conflicts with an existing row); see `orun secrets list --chain --env <env>`", key, err)
case apiErr.IsNotFound():
return fmt.Errorf("cannot write %s: %w\nhint: the scope was not found or you lack access; check `orun cloud status`", key, err)
// Resource-hiding masks authorization as not_found. The "scope
// missing" reading is usually WRONG and cost a live bootstrap a
// half-hour of misdiagnosis: when reads on the same scope work
// (listing succeeded moments before), the credential's ROLE is
// below the write floor — secret writes need an ADMIN-role key.
return fmt.Errorf("cannot write %s: %w\nhint: not_found on a WRITE usually means the credential lacks write access (denials are hidden as not_found). If reads on this scope work, the API key's role is below ADMIN — re-mint it with the admin role. A genuinely missing scope would also fail reads: `orun secrets list --org <ws> --project`", key, err)
}
}
return err
Expand Down
7 changes: 7 additions & 0 deletions cmd/orun/command_workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ always names the rung that actually won.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 1 {
// "show"/"current" are what people naturally type for the
// bare behavior — resolving them as workspace NAMES sent a
// nonsense request upstream (GET /organizations/show, live).
switch args[0] {
case "show", "current":
return runWorkspaceShow()
}
return runWorkspaceShowRemote(cmd.Context(), args[0])
}
return runWorkspaceShow()
Expand Down
12 changes: 12 additions & 0 deletions cmd/orun/commands_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,18 @@ func init() {
registerAuthCommand(rootCmd)
registerCloudCommand(rootCmd)
registerWorkspaceCommand(rootCmd)
// `orun version` — the spelling every other CLI accepts. Muscle memory
// (and agent runbooks) type it constantly; erroring with "unknown
// command" reads as a broken install (hit live: an operator reinstalled
// a perfectly good binary over it).
rootCmd.AddCommand(&cobra.Command{
Use: "version",
Short: "Print the orun version (same as --version)",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("%s version %s\n", cliName, version)
},
})
registerBackendCommand(rootCmd)
registerValidateCommand(rootCmd)
registerDebugCommand(rootCmd)
Expand Down
2 changes: 1 addition & 1 deletion internal/composition/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,4 @@ func secureExtractPath(root, name string) (string, error) {
return "", fmt.Errorf("archive entry escapes destination: %s", name)
}
return targetPath, nil
}
}
6 changes: 3 additions & 3 deletions internal/composition/package_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ const (
compositionPackageLayerType = "application/vnd.sourceplane.orun.composition.package.layer.v1.tar+gzip"

// Stack OCI media types (orun.io/v1 / kind: Stack format).
stackArtifactType = "application/vnd.orun.stack.v1"
compositionsLayerMediaType = "application/vnd.orun.stack.compositions.layer.v1+tar+gzip"
examplesLayerMediaType = "application/vnd.orun.stack.examples.layer.v1+tar+gzip"
stackArtifactType = "application/vnd.orun.stack.v1"
compositionsLayerMediaType = "application/vnd.orun.stack.compositions.layer.v1+tar+gzip"
examplesLayerMediaType = "application/vnd.orun.stack.examples.layer.v1+tar+gzip"
)

// BuildPackageArchive validates a composition package directory and writes a .tgz archive to disk.
Expand Down
12 changes: 6 additions & 6 deletions internal/composition/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ func TestSanitizeTag(t *testing.T) {

func TestNormalizeOCIRef(t *testing.T) {
cases := map[string]string{
"sourceplane/devops-compositions": "ghcr.io/sourceplane/devops-compositions:latest",
"ghcr.io/acme/aws-vpc": "ghcr.io/acme/aws-vpc:latest",
"ghcr.io/acme/aws-vpc:v1": "ghcr.io/acme/aws-vpc:v1",
"oci://ghcr.io/acme/aws-vpc:v1": "ghcr.io/acme/aws-vpc:v1",
"localhost:5000/acme/x:dev": "localhost:5000/acme/x:dev",
"": "",
"sourceplane/devops-compositions": "ghcr.io/sourceplane/devops-compositions:latest",
"ghcr.io/acme/aws-vpc": "ghcr.io/acme/aws-vpc:latest",
"ghcr.io/acme/aws-vpc:v1": "ghcr.io/acme/aws-vpc:v1",
"oci://ghcr.io/acme/aws-vpc:v1": "ghcr.io/acme/aws-vpc:v1",
"localhost:5000/acme/x:dev": "localhost:5000/acme/x:dev",
"": "",
}
for in, want := range cases {
if got := NormalizeOCIRef(in); got != want {
Expand Down
23 changes: 20 additions & 3 deletions internal/composition/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import (
"path/filepath"
"sort"
"strings"
"time"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/santhosh-tekuri/jsonschema/v5"
"github.com/sourceplane/orun/internal/model"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -1441,9 +1443,24 @@ func resolveOCIDigest(remoteRef string) (string, error) {
return "", err
}

desc, err := repo.Resolve(ctx, ref)
if err != nil {
return "", fmt.Errorf("failed to resolve OCI ref %s: %w", remoteRef, err)
// Registries throw transient 5xx/timeouts; a single-shot resolve took
// down a whole apply (and poisoned its retry) over a GHCR 503 that
// cleared seconds later. Three attempts with backoff; non-transient
// errors (404, auth) fail on the first try.
var desc ocispec.Descriptor
for attempt := 1; ; attempt++ {
desc, err = repo.Resolve(ctx, ref)
if err == nil {
break
}
msg := err.Error()
transient := strings.Contains(msg, "503") || strings.Contains(msg, "502") ||
strings.Contains(msg, "500") || strings.Contains(msg, "timeout") ||
strings.Contains(msg, "connection reset") || strings.Contains(msg, "EOF")
if !transient || attempt >= 3 {
return "", fmt.Errorf("failed to resolve OCI ref %s: %w", remoteRef, err)
}
time.Sleep(time.Duration(attempt*3) * time.Second)
}
return desc.Digest.String(), nil
}
Loading