Skip to content
Open
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
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ common.mk:
printf '%s' '$(DEV_KIT_VERSION)' > .common.mk-version
touch .common.mk-checked

DOCKER ?= docker

.PHONY: fmt
fmt: $(GOLANGCI_LINT) ## Format code
$(GO) fmt ./...
Expand Down
85 changes: 77 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ ocm-kit "http://localhost:5000/my-components//opendefense.cloud/arc:0.1.0" \

- `-r, --chart-resource string` - Name of the Helm chart resource in the component (default: "")
- `-f, --local-helm-values-template string` - Path to a local Helm values template file (overrides component template)
- `-p, --pull-secrets-file string` - Path to a pull secrets JSON file mapping registries to Kubernetes secret names
- `-h, --help` - Display help message

### Registry Credentials
Expand Down Expand Up @@ -163,6 +164,69 @@ config, so you can override individual hosts when needed.
See the [OCM credentials tutorial](https://ocm.software/docs/tutorials/credentials-in-an-.ocmconfig-file/)
for the full set of identity types and credential providers.

### Pull Secrets

When rendering the Helm values template, you may need to attach
`imagePullSecrets` to deployments. The `--pull-secrets-file` flag and the
`pullSecretFor` template function work together to map OCI registries to
Kubernetes Secret names.

This approach allows the template author to specify where to add
`imagePullSecrets` in a `values.yaml` while the deployer has control over
deployment specific data like the concrete secret names.

The pull secrets file uses the following format:

```json
{
"$schema": "https://raw.githubusercontent.com/opendefensecloud/ocm-kit/refs/heads/main/helmvalues/pullsecrets-schema.json",
"pullSecrets": [
{
"registry": "docker.io",
"secretName": "docker-hub-cred"
},
{
"registry": "ghcr.io/my-org",
"secretName": "ghcr-org-cred"
},
{
"registry": "localhost:5000",
"secretName": "regcred"
}
]
}
```

The `pullSecretFor` function in templates resolves an OCI reference (hostname,
hostname/repo, or full image ref) to the matching secret name. Resolution walks
from most-specific to least-specific path segments:

- `pullSecretFor "ghcr.io/my-org/my-repo:latest"` checks `ghcr.io/my-org/my-repo` -> `ghcr.io/my-org` -> `ghcr.io`
- `pullSecretFor "docker.io"` checks the registry host directly

If no match is found `pullSecretFor` returns the empty string.

It is advised that templates may always be written in a way that gracefully
handle `pullSecretFor` returning an empty string value. Like in the example
below, template authors can make use of go-template's `with` expression.

Example template usage:

```yaml
{{- $image := index .OCIResources "my-image" }}
{{- with pullSecretFor $image.Host }}
imagePullSecrets:
- name: {{ . }}
{{- end }}
```
Comment thread
olzemal marked this conversation as resolved.

CLI invocation:

```bash
ocm-kit "https://example.com/my-components//example.com/my-component:0.1.0" \
--pull-secrets-file ./pull-secrets.json
```

### Example

#### Values Template
Expand Down Expand Up @@ -210,7 +274,7 @@ import (
"context"
"fmt"
"log"

"go.opendefense.cloud/ocm-kit/helmvalues"
"go.opendefense.cloud/ocm-kit/compver"
"ocm.software/ocm/api/ocm"
Expand All @@ -219,46 +283,46 @@ import (

func main() {
ctx := context.Background()

// Parse component version reference
cvr, err := compver.SplitRef("http://localhost:5000/my-components//opendefense.cloud/arc:0.1.0")
if err != nil {
log.Fatal(err)
}

// Setup OCM repository
octx := ocm.FromContext(ctx)
repo, err := octx.RepositoryForSpec(ocireg.NewRepositorySpec(cvr.BaseURL()))
if err != nil {
log.Fatal(err)
}
defer repo.Close()

// Get component version
compVer, err := repo.LookupComponentVersion(cvr.ComponentName, cvr.Version)
if err != nil {
log.Fatal(err)
}
defer compVer.Close()

// Find helm values template for a specific chart
tmpl, err := helmvalues.GetHelmValuesTemplate(compVer, "helm-chart")
if err != nil {
log.Fatal(err)
}

// Get rendering input with component data
input, err := helmvalues.GetRenderingInput(compVer)
if err != nil {
log.Fatal(err)
}

// Render the template
renderedValues, err := helmvalues.Render(tmpl, input)
if err != nil {
log.Fatal(err)
}

fmt.Println(renderedValues)
}
```
Expand All @@ -285,6 +349,11 @@ repository: {{ $image.host }}/{{ $image.repository }}
tag: {{ $image.tag }}
```

### `.PullSecrets`
A map of registries to secret-names. Use the `pullSecretFor` template function
to refer to possible `imagePullSecrets`. It implements a hierarchical
path-resolution logic (See [Pull Secrets](#pull-secrets)).

### `.Component`
Component metadata available as a `compdesc.ComponentSpec`, providing access to:
- Component name and version
Expand Down
10 changes: 10 additions & 0 deletions cmd/ocm-kit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func main() {
var (
chartResName string
localHelmValuesTemplate string
pullSecretsFilePath string
)

rootCmd := &cobra.Command{
Expand Down Expand Up @@ -81,6 +82,14 @@ It takes a component version reference and renders the first Helm values templat
return fmt.Errorf("failed to build rendering input: %w", err)
}

if pullSecretsFilePath != "" {
ps, err := helmvalues.ParsePullSecretsFile(pullSecretsFilePath)
if err != nil {
return fmt.Errorf("failed to parse pull secrets file: %w", err)
}
input.PullSecrets = ps
}

output, err := helmvalues.Render(template, input)
if err != nil {
return fmt.Errorf("failed to render helm values template: %w", err)
Expand All @@ -93,6 +102,7 @@ It takes a component version reference and renders the first Helm values templat

rootCmd.Flags().StringVarP(&chartResName, "chart-resource", "r", "", "Name of the Helm chart resource in the component to render a specific helm values template")
rootCmd.Flags().StringVarP(&localHelmValuesTemplate, "local-helm-values-template", "f", "", "Path to a local Helm values template file (overrides component template)")
rootCmd.Flags().StringVarP(&pullSecretsFilePath, "pull-secrets-file", "p", "", "Path to a pull secrets JSON file mapping registries to Kubernetes secret names")

if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
Expand Down
51 changes: 45 additions & 6 deletions helmvalues/helmvalues.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"slices"
"strings"
"text/template"

"github.com/Masterminds/sprig/v3"
Expand All @@ -24,10 +25,8 @@ const (
HelmValuesTemplateLabelName = "opendefense.cloud/helm/values-for"
)

var (
// ErrNotFound is returned when a requested Helm values template is not found
ErrNotFound = errors.New("not found")
)
// ErrNotFound is returned when a requested Helm values template is not found
var ErrNotFound = errors.New("not found")

// HelmValuesTemplate represents a Helm values template found in an OCM component.
// It contains the template content along with metadata about its resource.
Expand All @@ -46,11 +45,47 @@ type ImageReference struct {
Digest string
}

// PullSecrets is a collection of registry-to-secret mappings for pull secrets.
type PullSecrets map[string]string

// Get safely returns the secret name for a registry, falling back to the empty string.
func (p PullSecrets) Get(registry string) string {
if p == nil {
return ""
}
return p[registry]
}

// Resolve parses ref as an OCI reference and walks the path from most specific
// to least specific (Host/path/to/image -> Host/path/to -> … -> Host).
// If no match is found this way it tries to lookup the raw string.
func (p PullSecrets) Resolve(ref string) string {
if !strings.Contains(ref, "/") {
return p.Get(ref)
}
parsed, err := ParseOCIRef(ref)
if err != nil || parsed.Repository == "" {
return p.Get(ref)
}
parts := strings.Split(parsed.Repository, "/")
for i := len(parts); i >= 0; i-- {
key := parsed.Host
if i > 0 {
key += "/" + strings.Join(parts[:i], "/")
}
if secret := p.Get(key); secret != "" {
return secret
}
}
return p.Get(ref)
}

// RenderingInput contains all the data needed to render a Helm values template.
// It provides access to component resources and the component descriptor for template processing.
type RenderingInput struct {
OCIResources map[string]ImageReference
Component *compdesc.ComponentSpec
PullSecrets PullSecrets
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// RenderOption is a functional option for configuring Render behavior
Expand Down Expand Up @@ -291,7 +326,7 @@ func Render(tmpl *HelmValuesTemplate, input *RenderingInput, opts ...RenderOptio
// Create template with custom function map
t, err := template.New(tmpl.ResourceName).
Option("missingkey=error").
Funcs(getFuncMap()).
Funcs(getFuncMap(input.PullSecrets)).
Parse(tmpl.TemplateContent)
if err != nil {
return "", fmt.Errorf("failed to parse template: %w", err)
Expand Down Expand Up @@ -350,7 +385,7 @@ func matchLabelValue(value any, target string) bool {
// plus custom functions for JSON conversion and OCI reference parsing.
//
// Returns a template.FuncMap with all available template functions.
func getFuncMap() template.FuncMap {
func getFuncMap(pullSecrets PullSecrets) template.FuncMap {
f := sprig.TxtFuncMap()
// Remove potentially unsafe functions
delete(f, "env")
Expand All @@ -367,6 +402,10 @@ func getFuncMap() template.FuncMap {

f["parseRef"] = ParseOCIRef

f["pullSecretFor"] = func(ref string) string {
return pullSecrets.Resolve(ref)
}

return f
}

Expand Down
Loading
Loading