Skip to content
4 changes: 4 additions & 0 deletions chart/templates/replicated-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ spec:
- name: REPLICATED_HA_ENABLED
value: "true"
{{- end }}
{{- if .Values.devOffline }}
- name: REPLICATED_DEV_OFFLINE
value: "true"
{{- end }}
{{- if (.Values.integration).licenseID }}
- name: REPLICATED_INTEGRATION_LICENSE_ID
valueFrom:
Expand Down
4 changes: 4 additions & 0 deletions chart/templates/replicated-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ rules:
- 'secrets'
verbs:
- 'update'
# The names below must match the SDK's runtime expectations. If you
# rename one, update the corresponding constant on the Go side:
# replicated-meta-data → pkg/meta/meta.ReplicatedMetadataSecretName
# replicated-support-metadata → pkg/supportbundle.SupportBundleMetadataSecretName
resourceNames:
- {{ include "replicated.secretName" . }}
- replicated-instance-report
Expand Down
7 changes: 7 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,13 @@ reportAllImages: false
# See docs for feature availability in this mode.
readOnlyMode: false

# When true (and license type is "dev"), the SDK treats the install as
# airgap — it never calls replicated.app and serves the license from the
# chart-embedded bytes. Intended for local development with a dev license
# when working offline (VPN, flaky network, air-gapped lab). Setting this
# with a non-dev license causes the SDK to refuse to start.
devOffline: false

# Proxy configuration for outbound connections
# Configure HTTPS proxy settings for the Replicated SDK
# These values can also be set via global.replicated.httpsProxy and global.replicated.noProxy
Expand Down
5 changes: 5 additions & 0 deletions cmd/replicated/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ func APICmd() *cobra.Command {
namespace := v.GetString("namespace")
configFilePath := v.GetString("config-file")
integrationLicenseID := v.GetString("integration-license-id")
// dev-offline auto-binds to REPLICATED_DEV_OFFLINE via
// root.go's SetEnvPrefix("REPLICATED") + AutomaticEnv +
// SetEnvKeyReplacer("-", "_"); no explicit BindEnv needed.
devOffline := v.GetBool("dev-offline")

if configFilePath == "" && integrationLicenseID == "" {
return errors.New("either config file or integration license id must be specified")
Expand Down Expand Up @@ -78,6 +82,7 @@ func APICmd() *cobra.Command {
ReportAllImages: replicatedConfig.ReportAllImages,
ReadOnlyMode: replicatedConfig.ReadOnlyMode,
Namespace: namespace,
DevOffline: devOffline,
}
apiserver.Start(params)

Expand Down
71 changes: 60 additions & 11 deletions dagger/e2e.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,24 +267,47 @@ spec:
}
fmt.Println(out)

// wait for the replicated-ssl-test deployment to be ready
// Wait for the replicated-ssl-test deployment to be ready. The probe
// inside the pod (curl -k https://replicated:3000/health) only passes
// once the replicated service has Ready endpoints AND TLS is serving,
// but the pod itself also has to be scheduled, image-pulled, and
// started. On slower CI clusters (notably GKE during image-pull
// contention) a 1m budget is not enough — observed failures show the
// alpine/curl pod still in ContainerCreating at the timeout. 3m gives
// the cluster room to schedule without masking real readiness failures.
ctr = dag.Container().From("bitnami/kubectl:latest").
WithFile(kubeconfigPath, kubeconfigSource.File("/kubeconfig")).
WithEnvVariable("KUBECONFIG", kubeconfigPath).
WithExec([]string{"kubectl", "wait", "--for=condition=available", "deployment/replicated-ssl-test", "--timeout=1m"})
WithExec([]string{"kubectl", "wait", "--for=condition=available", "deployment/replicated-ssl-test", "--timeout=3m"})
out, err = ctr.Stdout(ctx)
if err != nil {
ctr = dag.Container().From("bitnami/kubectl:latest").
WithFile(kubeconfigPath, kubeconfigSource.File("/kubeconfig")).
WithEnvVariable("KUBECONFIG", kubeconfigPath).
WithExec([]string{"kubectl", "logs", "-p", "-l", "app.kubernetes.io/name=replicated"})
out, err2 := ctr.Stdout(ctx)
if err2 != nil {
return fmt.Errorf("failed to get logs for replicated deployment: %w", err2)
// Best-effort diagnostics. None of these may individually fail
// the test — the original wait error is what we report. In
// particular, kubectl logs without -p must be used here: the
// previous use of "-p" returned BadRequest ("previous
// terminated container ... not found") whenever the replicated
// pod had not crashed, which masked the real wait timeout with
// a misleading "log fetch" error.
dumpCmd := func(args []string) {
c := dag.Container().From("bitnami/kubectl:latest").
WithFile(kubeconfigPath, kubeconfigSource.File("/kubeconfig")).
WithEnvVariable("KUBECONFIG", kubeconfigPath).
With(CacheBustingExec(args))
if stdout, derr := c.Stdout(ctx); derr == nil {
fmt.Printf("$ %s\n%s\n", strings.Join(args, " "), stdout)
} else if stderr, _ := c.Stderr(ctx); stderr != "" {
fmt.Printf("$ %s\n(diagnostic command failed: %v)\n%s\n", strings.Join(args, " "), derr, stderr)
} else {
fmt.Printf("$ %s\n(diagnostic command failed: %v)\n", strings.Join(args, " "), derr)
}
}
fmt.Println(out)
dumpCmd([]string{"kubectl", "get", "pods", "-o", "wide"})
dumpCmd([]string{"kubectl", "describe", "deployment/replicated-ssl-test"})
dumpCmd([]string{"kubectl", "describe", "pods", "-l", "app=replicated-ssl-test"})
dumpCmd([]string{"kubectl", "logs", "-l", "app=replicated-ssl-test", "--tail=100"})
dumpCmd([]string{"kubectl", "logs", "-l", "app.kubernetes.io/name=replicated", "--tail=100"})

return fmt.Errorf("failed to wait for replicated deployment to be ready: %w", err)
return fmt.Errorf("failed to wait for replicated-ssl-test deployment to be ready: %w", err)
}
fmt.Println(out)

Expand Down Expand Up @@ -1516,3 +1539,29 @@ func upgradeChartAndRestart(

return nil
}

// Future: dagger e2e scenarios for the devOffline opt-in. Tracked as a
// follow-up PR. The unit tests in pkg/apiserver/bootstrap_test.go
// (TestApplyDevOfflineGuard_*) already cover both validation paths at
// the logic level. Scenario C (default behavior unchanged,
// devOffline=false) is already covered by the existing e2e flow above.
// Scenarios A and B need dagger plumbing that does not exist yet — a
// dev-license fixture alongside the e2e secrets, and a NetworkPolicy
// denying egress to replicated.app from the SDK pod.
//
// Scenario A (devOffline=true + dev license + blocked upstream):
//
// helm install ... \
// --set-file license=$DEV_LICENSE \
// --set devOffline=true
// kubectl apply -f testdata/networkpolicy-deny-replicated-app.yaml
// # Expected: pod becomes Ready, /api/v1/license/info serves from
// # chart bytes, no upstream dial attempted.
//
// Scenario B (devOffline=true + non-dev license):
//
// helm install ... \
// --set-file license=$PROD_LICENSE \
// --set devOffline=true
// # Expected: pod CrashLoopBackOff with "devOffline=true requires a
// # dev license" in the logs.
Loading