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: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.23-alpine AS builder
FROM golang:1.24-alpine AS builder

WORKDIR /app

Expand Down
2 changes: 1 addition & 1 deletion ccrn-chart/templates/crds/ccrn.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ spec:
properties:
ccrn:
type: string
pattern: "^ccrn: .+"
pattern: "^ccrn=.+"
urn:
type: string
pattern: "^urn:ccrn:.+"
3 changes: 3 additions & 0 deletions ccrn-chart/templates/crds/rhel_storage/server.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company
# SPDX-License-Identifier: Apache-2.0

Expand Down
3 changes: 3 additions & 0 deletions ccrn-chart/templates/crds/rhel_storage/volume.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company
# SPDX-License-Identifier: Apache-2.0

Expand Down
18 changes: 10 additions & 8 deletions ccrn-chart/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ metadata:
labels:
{{- include "ccrn.labels" . | nindent 4 }}
rules:
# CCRN custom resource (validate group)
- apiGroups: ["validate.{{ .Values.ccrn.apiGroup }}"]
resources: ["ccrns", "ccrns/status"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["keystone.openstack.{{ .Values.ccrn.apiGroup }}"]
# Target resource groups (dry-run create for validation)
- apiGroups:
- "vmware.{{ .Values.ccrn.apiGroup }}"
- "opensearch.{{ .Values.ccrn.apiGroup }}"
- "ise.{{ .Values.ccrn.apiGroup }}"
- "keystone.openstack.{{ .Values.ccrn.apiGroup }}"
- "rhel-storage.{{ .Values.ccrn.apiGroup }}"
resources: ["*"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["vault.{{ .Values.ccrn.apiGroup }}"]
resources: ["*"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["*.{{ .Values.ccrn.apiGroup }}"]
resources: ["*"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
# CRD access for schema discovery
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch"]
Expand Down
27 changes: 21 additions & 6 deletions cmd/webhook/main.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

// SPDX-FileCopyrightText: SAP SE or an SAP affiliate company
// SPDX-License-Identifier: Apache-2.0

package main

import (
"context"
"flag"
"os"
"os/signal"
"syscall"
"time"

"github.com/sirupsen/logrus"

Expand Down Expand Up @@ -55,12 +54,17 @@ func main() {
log.Fatalf("Failed to create webhook server: %v", err)
}

// Mark server as ready — NewWebhookServerFromConfig performs the initial
// backend.Refresh() which populates the CRD cache. If it failed critically,
// NewWebhookServerFromConfig would have returned an error above.
server.SetReady()
Comment on lines +57 to +60

// Set up signal handling for graceful shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

// Start the webhook server in a goroutine
errCh := make(chan error)
errCh := make(chan error, 1)
go func() {
errCh <- server.Serve(port, certFile, keyFile)
}()
Expand All @@ -69,7 +73,18 @@ func main() {
select {
case err := <-errCh:
log.Fatalf("Webhook server failed: %v", err)
case <-stop:
log.Info("Received shutdown signal, exiting...")
case sig := <-stop:
log.Infof("Received signal %v, initiating graceful shutdown...", sig)
}

// Graceful shutdown with 15-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

if err := server.Shutdown(ctx); err != nil {
log.Errorf("Graceful shutdown failed: %v", err)
os.Exit(1)
}

log.Info("Server shut down gracefully")
}
35 changes: 24 additions & 11 deletions pkg/apis/parsed_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
package apis

import (
"fmt"
"sort"
"strings"
)

Expand All @@ -13,7 +13,7 @@ type ParsedResource struct {
Format string // "CCRN" or "URN"
Fields map[string]string
Raw string
UrnTemplate string // URN template used for parsing, if applicable
URNTemplate string // URN template used for parsing, if applicable
}

// CCRN returns the full CCRN string from the parsed resource
Expand All @@ -22,20 +22,33 @@ func (p *ParsedResource) CCRN() string {
if !exists {
return ""
}
ccrn := "ccrn=" + ccrnString
for key, value := range p.Fields {

// Collect non-ccrn keys and sort them for deterministic output
keys := make([]string, 0, len(p.Fields)-1)
for key := range p.Fields {
if key != "ccrn" {
ccrn += fmt.Sprintf(", %s=%s", key, value)
keys = append(keys, key)
}
}
return ccrn
sort.Strings(keys)

var b strings.Builder
b.WriteString("ccrn=")
b.WriteString(ccrnString)
for _, key := range keys {
b.WriteString(", ")
b.WriteString(key)
b.WriteString("=")
b.WriteString(p.Fields[key])
}
return b.String()
}

// URN returns the URN string from the parsed resource using the provided template
func (p *ParsedResource) URN(template string) string {
if template == "" {
if p.UrnTemplate != "" {
template = p.UrnTemplate
if p.URNTemplate != "" {
template = p.URNTemplate
} else {
return ""
}
Expand Down Expand Up @@ -77,8 +90,8 @@ func (p *ParsedResource) GetKind() string {
return ""
}

// ApiGroup returns the group from the parsed CCRN or URN
func (p *ParsedResource) ApiGroup() string {
// APIGroup returns the group from the parsed CCRN or URN
func (p *ParsedResource) APIGroup() string {
if ccrn, ok := p.Fields["ccrn"]; ok {
resourceParts := strings.SplitN(strings.SplitN(ccrn, "/", 2)[0], ".", 2)
if len(resourceParts) < 2 {
Expand All @@ -89,7 +102,7 @@ func (p *ParsedResource) ApiGroup() string {
return ""
}

// ApiGroup returns the group from the parsed CCRN or URN
// CCRNName returns the name from the parsed CCRN or URN
func (p *ParsedResource) CCRNName() string {
if ccrn, ok := p.Fields["ccrn"]; ok {
name := strings.SplitN(ccrn, "/", 2)[0]
Expand Down
3 changes: 0 additions & 3 deletions pkg/apis/types.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

// SPDX-FileCopyrightText: SAP SE or an SAP affiliate company
// SPDX-License-Identifier: Apache-2.0

package apis

import (
Expand Down
2 changes: 1 addition & 1 deletion pkg/apis/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type ValidationBackend interface {
GetCRD(ccrnVersion string) (*CRDInfo, error)

// ValidateResource validates a resource against its schema
// For KubernetesBackend, this creates an actual resource
// For KubernetesBackend, this uses dry-run create (no resource is persisted)
// For FilesystemBackend, this validates against OpenAPI schema
ValidateResource(namespace string, parsedCCRN *ParsedResource) error

Expand Down
150 changes: 150 additions & 0 deletions pkg/ccrn/ccrn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

// Package ccrn provides zero-dependency types and utilities for Common Cloud Resource Names.
// This package only imports from the Go standard library (production code).
package ccrn

import (
"sort"
"strings"
)

// Format constants for resource representations.
const (
FormatCCRN = "CCRN"
FormatURN = "URN"
)

// DefaultDomain is the default API group domain for CCRN resources.
const DefaultDomain = "ccrn.cloudoperators.dev"

// APIGroup is the Kubernetes API group for CCRN resources.
const APIGroup = "ccrn.cloudoperators.dev"

// ParsedResource represents a parsed CCRN or URN resource with its fields.
type ParsedResource struct {
// Format indicates the source format: FormatCCRN or FormatURN.
Format string

// Fields contains the key-value pairs of the resource (excluding the ccrn key itself).
Fields map[string]string

// RawInput is the original input string that was parsed.
RawInput string

// CCRNKey is the CCRN type key (e.g., "pod.k8s.ccrn.cloudoperators.dev/v1").
CCRNKey string
}

// CCRN returns the deterministic CCRN string representation with fields sorted alphabetically.
// Returns empty string if CCRNKey is empty.
func (p ParsedResource) CCRN() string {
if p.CCRNKey == "" {
return ""
}

var b strings.Builder
b.WriteString("ccrn=")
b.WriteString(p.CCRNKey)

if len(p.Fields) == 0 {
return b.String()
}

// Sort field keys alphabetically for deterministic output
keys := make([]string, 0, len(p.Fields))
for k := range p.Fields {
keys = append(keys, k)
}
sort.Strings(keys)

for _, k := range keys {
b.WriteString(", ")
b.WriteString(k)
b.WriteString("=")
b.WriteString(p.Fields[k])
}

return b.String()
}

// URN returns the URN string representation using the given template.
// Template format: "/{field1}/{field2}/..." where {fieldN} are placeholders for field values.
// Returns empty string if template or CCRNKey is empty.
func (p ParsedResource) URN(template string) string {
if template == "" || p.CCRNKey == "" {
return ""
}

var b strings.Builder
b.WriteString("urn:ccrn:")
b.WriteString(p.CCRNKey)

// Parse template segments - template starts with /
// e.g., "/{cluster}/{namespace}/{name}" -> ["", "{cluster}", "{namespace}", "{name}"]
segments := strings.Split(template, "/")

for _, seg := range segments {
if seg == "" {
continue
}

b.WriteString("/")

// Check if segment is a placeholder like {fieldName}
if strings.HasPrefix(seg, "{") && strings.HasSuffix(seg, "}") {
fieldName := seg[1 : len(seg)-1]
if val, ok := p.Fields[fieldName]; ok {
b.WriteString(val)
} else {
// Leave placeholder as-is if field not found
b.WriteString(seg)
}
} else {
b.WriteString(seg)
}
}

return b.String()
}

// IsCCRNGroup checks if a group belongs to the given CCRN domain.
// A group belongs to the domain if it equals the domain or ends with ".<domain>".
func IsCCRNGroup(group, domain string) bool {
if group == domain {
return true
}
return strings.HasSuffix(group, "."+domain)
}

// Match performs wildcard matching where "*" matches any single segment value.
// Returns true if pattern equals "*" or pattern equals value exactly.
func Match(pattern, value string) bool {
if pattern == "*" {
return true
}
return pattern == value
}

// MatchAll matches all fields of a pattern against a resource.
// Fields absent from the pattern are treated as implicit wildcards (always match).
// Fields present in the pattern with value "*" also match any value.
// If a pattern field key is not present in the resource, it does not match
// (unless the pattern value is "*").
func MatchAll(pattern, resource ParsedResource) bool {
for key, patternValue := range pattern.Fields {
resourceValue, exists := resource.Fields[key]
if !exists {
// Key not in resource: only matches if pattern is wildcard
if patternValue != "*" {
return false
}
continue
}
if !Match(patternValue, resourceValue) {
return false
}
}
return true
}
16 changes: 16 additions & 0 deletions pkg/ccrn/ccrn_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package ccrn_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestCCRN(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "CCRN Package Suite")
}
Loading