forked from cerc-io/stack-orchestrator
Add private registry authentication support
Add ability to configure private container registry credentials in spec.yml
for deployments using images from registries like GHCR.
- Add get_image_registry_config() to spec.py for parsing image-registry config
- Add create_registry_secret() to create K8s docker-registry secrets
- Update cluster_info.py to use dynamic {deployment}-registry secret names
- Update deploy_k8s.py to create registry secret before deployment
- Document feature in deployment_patterns.md
The token-env pattern keeps credentials out of git - the spec references an
environment variable name, and the actual token is passed at runtime.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d82b3fb881
commit
73ba13aaa5
@ -75,3 +75,52 @@ This overwrites your customizations with defaults from the stack's `commands.py`
|
|||||||
git pull # Get latest spec.yml from your operator repo
|
git pull # Get latest spec.yml from your operator repo
|
||||||
laconic-so deployment --dir my-deployment restart
|
laconic-so deployment --dir my-deployment restart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Private Registry Authentication
|
||||||
|
|
||||||
|
For deployments using images from private container registries (e.g., GitHub Container Registry), configure authentication in your spec.yml:
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Add an `image-registry` section to your spec.yml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
image-registry:
|
||||||
|
server: ghcr.io
|
||||||
|
username: your-org-or-username
|
||||||
|
token-env: REGISTRY_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
- `server`: The registry hostname (e.g., `ghcr.io`, `docker.io`, `gcr.io`)
|
||||||
|
- `username`: Registry username (for GHCR, use your GitHub username or org name)
|
||||||
|
- `token-env`: Name of the environment variable containing your API token/PAT
|
||||||
|
|
||||||
|
### Token Environment Variable
|
||||||
|
|
||||||
|
The `token-env` pattern keeps credentials out of version control. Set the environment variable when running `deployment start`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export REGISTRY_TOKEN="your-personal-access-token"
|
||||||
|
laconic-so deployment --dir my-deployment start
|
||||||
|
```
|
||||||
|
|
||||||
|
For GHCR, create a Personal Access Token (PAT) with `read:packages` scope.
|
||||||
|
|
||||||
|
### Ansible Integration
|
||||||
|
|
||||||
|
When using Ansible for deployments, pass the token from a credentials file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Start deployment
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: laconic-so deployment --dir {{ deployment_dir }} start
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN: "{{ lookup('file', '~/.credentials/ghcr_token') }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. laconic-so reads the `image-registry` config from spec.yml
|
||||||
|
2. Creates a Kubernetes `docker-registry` secret named `{deployment}-registry`
|
||||||
|
3. The deployment's pods reference this secret for image pulls
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
from importlib import util
|
from importlib import util
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import base64
|
import base64
|
||||||
@ -554,6 +555,87 @@ def _generate_and_store_secrets(config_vars: dict, deployment_name: str):
|
|||||||
return secrets
|
return secrets
|
||||||
|
|
||||||
|
|
||||||
|
def create_registry_secret(spec: Spec, deployment_name: str) -> Optional[str]:
|
||||||
|
"""Create K8s docker-registry secret from spec + environment.
|
||||||
|
|
||||||
|
Reads registry configuration from spec.yml and creates a Kubernetes
|
||||||
|
secret of type kubernetes.io/dockerconfigjson for image pulls.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: The deployment spec containing image-registry config
|
||||||
|
deployment_name: Name of the deployment (used for secret naming)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The secret name if created, None if no registry config
|
||||||
|
"""
|
||||||
|
from kubernetes import client, config as k8s_config
|
||||||
|
|
||||||
|
registry_config = spec.get_image_registry_config()
|
||||||
|
if not registry_config:
|
||||||
|
return None
|
||||||
|
|
||||||
|
server = registry_config.get("server")
|
||||||
|
username = registry_config.get("username")
|
||||||
|
token_env = registry_config.get("token-env")
|
||||||
|
|
||||||
|
if not all([server, username, token_env]):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Type narrowing for pyright - we've validated these aren't None above
|
||||||
|
assert token_env is not None
|
||||||
|
token = os.environ.get(token_env)
|
||||||
|
if not token:
|
||||||
|
print(
|
||||||
|
f"Warning: Registry token env var '{token_env}' not set, "
|
||||||
|
"skipping registry secret"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create dockerconfigjson format (Docker API uses "password" field for tokens)
|
||||||
|
auth = base64.b64encode(f"{username}:{token}".encode()).decode()
|
||||||
|
docker_config = {
|
||||||
|
"auths": {server: {"username": username, "password": token, "auth": auth}}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Secret name derived from deployment name
|
||||||
|
secret_name = f"{deployment_name}-registry"
|
||||||
|
|
||||||
|
# Load kube config
|
||||||
|
try:
|
||||||
|
k8s_config.load_kube_config()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
k8s_config.load_incluster_config()
|
||||||
|
except Exception:
|
||||||
|
print("Warning: Could not load kube config, registry secret not created")
|
||||||
|
return None
|
||||||
|
|
||||||
|
v1 = client.CoreV1Api()
|
||||||
|
namespace = "default"
|
||||||
|
|
||||||
|
k8s_secret = client.V1Secret(
|
||||||
|
metadata=client.V1ObjectMeta(name=secret_name),
|
||||||
|
data={
|
||||||
|
".dockerconfigjson": base64.b64encode(
|
||||||
|
json.dumps(docker_config).encode()
|
||||||
|
).decode()
|
||||||
|
},
|
||||||
|
type="kubernetes.io/dockerconfigjson",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
v1.create_namespaced_secret(namespace, k8s_secret)
|
||||||
|
print(f"Created registry secret '{secret_name}' for {server}")
|
||||||
|
except client.exceptions.ApiException as e:
|
||||||
|
if e.status == 409: # Already exists
|
||||||
|
v1.replace_namespaced_secret(secret_name, namespace, k8s_secret)
|
||||||
|
print(f"Updated registry secret '{secret_name}' for {server}")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
return secret_name
|
||||||
|
|
||||||
|
|
||||||
def _write_config_file(
|
def _write_config_file(
|
||||||
spec_file: Path, config_env_file: Path, deployment_name: Optional[str] = None
|
spec_file: Path, config_env_file: Path, deployment_name: Optional[str] = None
|
||||||
):
|
):
|
||||||
|
|||||||
@ -487,7 +487,12 @@ class ClusterInfo:
|
|||||||
volumes = volumes_for_pod_files(
|
volumes = volumes_for_pod_files(
|
||||||
self.parsed_pod_yaml_map, self.spec, self.app_name
|
self.parsed_pod_yaml_map, self.spec, self.app_name
|
||||||
)
|
)
|
||||||
image_pull_secrets = [client.V1LocalObjectReference(name="laconic-registry")]
|
registry_config = self.spec.get_image_registry_config()
|
||||||
|
if registry_config:
|
||||||
|
secret_name = f"{self.app_name}-registry"
|
||||||
|
image_pull_secrets = [client.V1LocalObjectReference(name=secret_name)]
|
||||||
|
else:
|
||||||
|
image_pull_secrets = []
|
||||||
|
|
||||||
annotations = None
|
annotations = None
|
||||||
labels = {"app": self.app_name}
|
labels = {"app": self.app_name}
|
||||||
|
|||||||
@ -332,6 +332,11 @@ class K8sDeployer(Deployer):
|
|||||||
else:
|
else:
|
||||||
print("Dry run mode enabled, skipping k8s API connect")
|
print("Dry run mode enabled, skipping k8s API connect")
|
||||||
|
|
||||||
|
# Create registry secret if configured
|
||||||
|
from stack_orchestrator.deploy.deployment_create import create_registry_secret
|
||||||
|
|
||||||
|
create_registry_secret(self.cluster_info.spec, self.cluster_info.app_name)
|
||||||
|
|
||||||
self._create_volume_data()
|
self._create_volume_data()
|
||||||
self._create_deployment()
|
self._create_deployment()
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,14 @@ class Spec:
|
|||||||
def get_image_registry(self):
|
def get_image_registry(self):
|
||||||
return self.obj.get(constants.image_registry_key)
|
return self.obj.get(constants.image_registry_key)
|
||||||
|
|
||||||
|
def get_image_registry_config(self) -> typing.Optional[typing.Dict]:
|
||||||
|
"""Returns registry auth config: {server, username, token-env}.
|
||||||
|
|
||||||
|
Used for private container registries like GHCR. The token-env field
|
||||||
|
specifies an environment variable containing the API token/PAT.
|
||||||
|
"""
|
||||||
|
return self.obj.get("image-registry")
|
||||||
|
|
||||||
def get_volumes(self):
|
def get_volumes(self):
|
||||||
return self.obj.get(constants.volumes_key, {})
|
return self.obj.get(constants.volumes_key, {})
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user