Skip to content

Commit b43c193

Browse files
committed
Add baseImage validation, ResolvedBaseImage status field, and CEL validation
1 parent 582f78c commit b43c193

File tree

11 files changed

+508
-5
lines changed

11 files changed

+508
-5
lines changed

config/crds/authzed.com_spicedbclusters.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ spec:
7474
baseImage:
7575
description: |-
7676
BaseImage specifies the base container image to use for SpiceDB.
77+
This is useful for air-gapped environments or when using a private registry.
78+
The operator will append the appropriate tag based on version/channel.
79+
Must not include a tag or digest - use spec.version or spec.config.image instead.
7780
If not specified, will fall back to the operator's --base-image flag,
7881
then to the imageName defined in the update graph.
7982
type: string
@@ -244,6 +247,12 @@ spec:
244247
description: Phase is the currently running phase (used for phased
245248
migrations)
246249
type: string
250+
resolvedBaseImage:
251+
description: |-
252+
ResolvedBaseImage is the base image that was resolved for this cluster.
253+
This shows which registry/image the operator is using before appending
254+
the version tag. Useful for debugging alternative registry configurations.
255+
type: string
247256
secretHash:
248257
description: SecretHash is a digest of the last applied secret
249258
type: string
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Using Alternative Container Registry
2+
3+
This example demonstrates how to configure the SpiceDB operator to use an alternative container registry instead of the default one.
4+
5+
## Overview
6+
7+
The SpiceDB operator supports specifying a custom base image for SpiceDB containers through the `baseImage` field in the `SpiceDBCluster` spec. This is useful when:
8+
9+
- You need to use a private container registry
10+
- You want to mirror images to your own registry for security or compliance reasons
11+
- You need to use a registry proxy for better performance
12+
- You're running in an air-gapped environment
13+
14+
## Configuration
15+
16+
The image selection follows this precedence order (highest to lowest):
17+
18+
1. `.spec.config.image` with explicit tag/digest (overrides everything)
19+
2. `.spec.baseImage` field (what this example uses)
20+
3. The operator's `--base-image` flag
21+
4. The `imageName` defined in the update graph
22+
23+
**Important:** The `baseImage` field must NOT contain a tag (`:tag`) or digest (`@sha256:...`). The operator will automatically append the appropriate tag based on the `version` or `channel` you specify. If you need to specify an exact image with tag, use `.spec.config.image` instead.
24+
25+
## Example
26+
27+
See [spicedb-cluster.yaml](spicedb-cluster.yaml) for a complete example.
28+
29+
```yaml
30+
apiVersion: authzed.com/v1alpha1
31+
kind: SpiceDBCluster
32+
metadata:
33+
name: example-with-custom-registry
34+
spec:
35+
# Specify your alternative registry here (NO TAG!)
36+
baseImage: "my-registry.company.com/authzed/spicedb"
37+
38+
# The operator will append the appropriate tag based on the version/channel
39+
version: "v1.33.0"
40+
41+
config:
42+
datastoreEngine: postgres
43+
# ... other config
44+
45+
# If using a private registry, use patches to add imagePullSecrets
46+
patches:
47+
- kind: Deployment
48+
patch: |
49+
spec:
50+
template:
51+
spec:
52+
imagePullSecrets:
53+
- name: registry-credentials
54+
```
55+
56+
## How it Works
57+
58+
When you specify a `baseImage`, the operator will:
59+
60+
1. Use your specified registry as the base
61+
2. Append the appropriate tag or digest based on the `version` or `channel` you specify
62+
3. The final image will be: `<baseImage>:<tag>` or `<baseImage>@<digest>`
63+
64+
For example, if you specify:
65+
66+
- `baseImage: "my-registry.company.com/authzed/spicedb"`
67+
- `version: "v1.33.0"`
68+
69+
The operator will use: `my-registry.company.com/authzed/spicedb:v1.33.0`
70+
71+
## Private Registry Authentication
72+
73+
If your alternative registry requires authentication, you need to:
74+
75+
1. Create an image pull secret with your registry credentials:
76+
77+
```bash
78+
kubectl create secret docker-registry registry-credentials \
79+
--docker-server=my-registry.company.com \
80+
--docker-username=YOUR-USERNAME \
81+
--docker-password=YOUR-PASSWORD \
82+
--namespace=spicedb-custom-registry
83+
```
84+
85+
2. Use the `patches` field to inject the image pull secret into the deployment:
86+
87+
```yaml
88+
spec:
89+
patches:
90+
- kind: Deployment
91+
patch: |
92+
spec:
93+
template:
94+
spec:
95+
imagePullSecrets:
96+
- name: registry-credentials
97+
```
98+
99+
## Common Mistakes
100+
101+
### Including a tag in baseImage
102+
103+
**Wrong:**
104+
105+
```yaml
106+
spec:
107+
baseImage: "my-registry.company.com/authzed/spicedb:v1.33.0" # Don't include tag!
108+
```
109+
110+
**Correct:**
111+
112+
```yaml
113+
spec:
114+
baseImage: "my-registry.company.com/authzed/spicedb"
115+
version: "v1.33.0"
116+
```
117+
118+
### Confusing baseImage with config.image
119+
120+
- Use `baseImage` when you want the operator to manage versions via the update graph
121+
- Use `config.image` (with full tag/digest) when you want to bypass the update graph entirely
122+
123+
## Important Notes
124+
125+
- Make sure your Kubernetes nodes can pull from your alternative registry
126+
- If using a private registry, use the `patches` field to configure image pull secrets
127+
- The operator still uses the update graph to determine valid versions and migration paths
128+
- The alternative registry must contain the exact same images as the official registry
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
apiVersion: v1
2+
kind: Namespace
3+
metadata:
4+
name: spicedb-custom-registry
5+
---
6+
apiVersion: authzed.com/v1alpha1
7+
kind: SpiceDBCluster
8+
metadata:
9+
name: example-with-custom-registry
10+
namespace: spicedb-custom-registry
11+
spec:
12+
# Use an alternative container registry
13+
# The operator will append the appropriate tag based on version/channel
14+
# NOTE: Do NOT include a tag here - just the registry and image name
15+
baseImage: "my-registry.company.com/authzed/spicedb"
16+
17+
# Specify the version to use
18+
version: "v1.33.0"
19+
20+
# Alternatively, use a channel for automatic updates within that channel
21+
# channel: "stable"
22+
23+
config:
24+
datastoreEngine: postgres
25+
logLevel: info
26+
27+
secretName: spicedb-config
28+
29+
# If using a private registry, use patches to add imagePullSecrets to the deployment
30+
patches:
31+
- kind: Deployment
32+
patch: |
33+
spec:
34+
template:
35+
spec:
36+
imagePullSecrets:
37+
- name: registry-credentials
38+
---
39+
apiVersion: v1
40+
kind: Secret
41+
metadata:
42+
name: spicedb-config
43+
namespace: spicedb-custom-registry
44+
stringData:
45+
datastore_uri: "postgresql://<CHANGE-ME-USERNAME>:<CHANGE-ME-PASSWORD>@<CHANGE-ME-POSTGRES-HOST>:5432/<CHANGE-ME-DATABASE>?sslmode=require"
46+
preshared_key: "<CHANGE-ME-TO-A-VERY-SECRET-PRESHARED-KEY>"
47+
---
48+
# If using a private registry, you may need an image pull secret
49+
apiVersion: v1
50+
kind: Secret
51+
metadata:
52+
name: registry-credentials
53+
namespace: spicedb-custom-registry
54+
type: kubernetes.io/dockerconfigjson
55+
data:
56+
# CHANGE-ME: This is a placeholder - replace with your actual registry credentials
57+
# Generate with: kubectl create secret docker-registry registry-credentials --docker-server=my-registry.company.com --docker-username=YOUR-USERNAME --docker-password=YOUR-PASSWORD --dry-run=client -o yaml
58+
.dockerconfigjson: <CHANGE-ME-BASE64-ENCODED-DOCKER-CONFIG>

examples/json-logging/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ spec:
4141
## Log Format Comparison
4242

4343
### Text Format (Default)
44-
```
44+
45+
```text
4546
2024-01-15T10:30:45.123Z INFO controller Reconciling SpiceDBCluster {"namespace": "default", "name": "my-spicedb"}
4647
2024-01-15T10:30:45.456Z INFO controller Created deployment {"namespace": "default", "name": "my-spicedb-spicedb"}
4748
```
4849

4950
### JSON Format
51+
5052
```json
5153
{"level":"info","ts":"2024-01-15T10:30:45.123Z","logger":"controller","msg":"Reconciling SpiceDBCluster","namespace":"default","name":"my-spicedb"}
5254
{"level":"info","ts":"2024-01-15T10:30:45.456Z","logger":"controller","msg":"Created deployment","namespace":"default","name":"my-spicedb-spicedb"}
@@ -57,6 +59,7 @@ spec:
5759
When using JSON logging with the ELK stack:
5860

5961
1. **Filebeat Configuration**: Configure Filebeat to read the operator logs:
62+
6063
```yaml
6164
filebeat.inputs:
6265
- type: container
@@ -83,4 +86,4 @@ When using JSON logging with the ELK stack:
8386
8487
## Complete Example
8588
86-
See the [operator-with-json-logging.yaml](operator-with-json-logging.yaml) file for a complete example of deploying the SpiceDB Operator with JSON logging enabled.
89+
See the [operator-with-json-logging.yaml](operator-with-json-logging.yaml) file for a complete example of deploying the SpiceDB Operator with JSON logging enabled.

pkg/apis/authzed/v1alpha1/types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ type ClusterSpec struct {
100100
Patches []Patch `json:"patches,omitempty"`
101101

102102
// BaseImage specifies the base container image to use for SpiceDB.
103+
// This is useful for air-gapped environments or when using a private registry.
104+
// The operator will append the appropriate tag based on version/channel.
105+
// Must not include a tag or digest - use spec.version or spec.config.image instead.
103106
// If not specified, will fall back to the operator's --base-image flag,
104107
// then to the imageName defined in the update graph.
105108
// +optional
@@ -143,6 +146,12 @@ type ClusterStatus struct {
143146
// Image is the image that is or will be used for this cluster
144147
Image string `json:"image,omitempty"`
145148

149+
// ResolvedBaseImage is the base image that was resolved for this cluster.
150+
// This shows which registry/image the operator is using before appending
151+
// the version tag. Useful for debugging alternative registry configurations.
152+
// +optional
153+
ResolvedBaseImage string `json:"resolvedBaseImage,omitempty"`
154+
146155
// Migration is the name of the last migration applied
147156
Migration string `json:"migration,omitempty"`
148157

@@ -169,6 +178,7 @@ func (s ClusterStatus) Equals(other ClusterStatus) bool {
169178
s.CurrentMigrationHash == other.TargetMigrationHash &&
170179
s.SecretHash == other.SecretHash &&
171180
s.Image == other.Image &&
181+
s.ResolvedBaseImage == other.ResolvedBaseImage &&
172182
s.Migration == other.Migration &&
173183
s.Phase == other.Phase &&
174184
s.CurrentVersion.Equals(other.CurrentVersion) &&

pkg/cmd/run/run_integration_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package run
22

33
import (
44
"bytes"
5-
"context"
65
"encoding/json"
76
"strings"
87
"testing"

pkg/config/config.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ type MigrationConfig struct {
143143
DatastoreURI string
144144
SpannerCredsSecretRef string
145145
TargetSpiceDBImage string
146+
ResolvedBaseImage string
146147
EnvPrefix string
147148
SpiceDBCmd string
148149
DatastoreTLSSecretName string
@@ -233,12 +234,32 @@ func NewConfig(cluster *v1alpha1.SpiceDBCluster, globalConfig *OperatorConfig, s
233234
// unless the current config is equal to the input.
234235
image := imageKey.pop(config)
235236

237+
// Validate that baseImage does not contain a tag or digest
238+
if cluster.Spec.BaseImage != "" {
239+
if strings.Contains(cluster.Spec.BaseImage, "@") {
240+
errs = append(errs, fmt.Errorf("baseImage must not contain a digest (@sha256:...) - version is determined by the update graph"))
241+
} else {
242+
// Check for tag - a tag appears after the last colon, but only if that colon
243+
// isn't part of a port number. Port numbers are followed by a slash (path),
244+
// while tags are at the end of the string.
245+
lastColon := strings.LastIndex(cluster.Spec.BaseImage, ":")
246+
if lastColon != -1 {
247+
afterColon := cluster.Spec.BaseImage[lastColon+1:]
248+
// If there's no slash after the colon, it's a tag (not a port)
249+
if !strings.Contains(afterColon, "/") {
250+
errs = append(errs, fmt.Errorf("baseImage must not contain a tag (:tag) - version is determined by the update graph. Use spec.version or spec.config.image instead"))
251+
}
252+
}
253+
}
254+
}
255+
236256
baseImage, targetSpiceDBVersion, state, err := globalConfig.ComputeTarget(globalConfig.ImageName, cluster.Spec.BaseImage, image, cluster.Spec.Version, cluster.Spec.Channel, datastoreEngine, cluster.Status.CurrentVersion, cluster.RolloutInProgress())
237257
if err != nil {
238258
errs = append(errs, err)
239259
}
240260

241261
migrationConfig.SpiceDBVersion = targetSpiceDBVersion
262+
migrationConfig.ResolvedBaseImage = baseImage
242263
migrationConfig.TargetPhase = state.Phase
243264
migrationConfig.TargetMigration = state.Migration
244265
if len(migrationConfig.TargetMigration) == 0 {

0 commit comments

Comments
 (0)