diff --git a/.github/workflows/build-deploy.yaml b/.github/workflows/build-deploy.yaml
new file mode 100644
index 0000000..d9a52b7
--- /dev/null
+++ b/.github/workflows/build-deploy.yaml
@@ -0,0 +1,47 @@
+name: build ocifit-k8s
+
+on:
+
+ # Publish packages on release
+ release:
+ types: [published]
+ pull_request: []
+
+ # On push to main we build and deploy images
+ push:
+ branches:
+ - main
+
+jobs:
+ build:
+ permissions:
+ packages: write
+
+ runs-on: ubuntu-latest
+ name: Build
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Build Container
+ if: (github.event_name != 'release')
+ run: make
+
+ - name: GHCR Login
+ if: (github.event_name != 'pull_request')
+ uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build Container
+ if: (github.event_name == 'release')
+ run: |
+ tag=${GITHUB_REF#refs/tags/}
+ make IMAGE_TAG=$tag
+ make push IMAGE_TAG=$tag
+
+ - name: Deploy
+ if: (github.event_name == 'push')
+ run: make push
diff --git a/README.md b/README.md
index 0c6a22f..8d766eb 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
+
+# OCIFit
+
diff --git a/cmd/ocifit/main.go b/cmd/ocifit/main.go
index 9e3111f..9c030fe 100644
--- a/cmd/ocifit/main.go
+++ b/cmd/ocifit/main.go
@@ -1,4 +1,3 @@
-// main.go
package main
import (
@@ -19,7 +18,6 @@ import (
"ghcr.io/compspec/ocifit-k8s/pkg/artifact"
"ghcr.io/compspec/ocifit-k8s/pkg/validator"
- ocispec "github.com/opencontainers/image-spec/specs-go/v1"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -31,8 +29,6 @@ import (
v1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
- "oras.land/oras-go/v2/content"
- "oras.land/oras-go/v2/registry/remote"
)
const (
@@ -108,7 +104,6 @@ func (ws *WebhookServer) recalculateHomogeneity() {
// We can't include control plane nodes - they don't have NFD labels
workerNodeSelector, err := labels.Parse("!node-role.kubernetes.io/control-plane")
if err != nil {
- // This is a static string, so this failure is fatal for the controller's logic.
log.Fatalf("FATAL: Failed to parse worker node selector: %v", err)
}
@@ -148,82 +143,17 @@ func (ws *WebhookServer) recalculateHomogeneity() {
ws.commonLabels = referenceLabels
}
-// --- WebhookServer with Node Cache ---
+// WebhookServer with Node Cache
type WebhookServer struct {
nodeLister v1listers.NodeLister
server *http.Server
- // Cached state and a lock to protect it ---
+ // Cached state and a lock to protect it
stateLock sync.RWMutex
isHomogenous bool
commonLabels map[string]string
}
-// findCompatibleImage uses ORAS to find an image that matches the requirements
-func findCompatibleImage(ctx context.Context, imageRef string, requirements map[string]string) (string, error) {
- registryName, repoAndTag, found := strings.Cut(imageRef, "/")
- if !found {
- return "", fmt.Errorf("invalid image reference format: %s", imageRef)
- }
- repoName, tag, found := strings.Cut(repoAndTag, ":")
- if !found {
- tag = "latest" // Default tag
- }
-
- // 1. Connect to the remote registry
- reg, err := remote.NewRegistry(registryName)
- if err != nil {
- return "", fmt.Errorf("failed to connect to registry %s: %w", registryName, err)
- }
- repo, err := reg.Repository(ctx, repoName)
- if err != nil {
- return "", fmt.Errorf("failed to access repository %s: %w", repoName, err)
- }
-
- // 2. Resolve the image index descriptor by its tag
- indexDesc, err := repo.Resolve(ctx, tag)
- if err != nil {
- return "", fmt.Errorf("failed to resolve image index %s:%s: %w", repoName, tag, err)
- }
-
- // 3. Fetch and unmarshal the image index
- indexBytes, err := content.FetchAll(ctx, repo, indexDesc)
- if err != nil {
- return "", fmt.Errorf("failed to fetch image index content: %w", err)
- }
- var index ocispec.Index
- if err := json.Unmarshal(indexBytes, &index); err != nil {
- return "", fmt.Errorf("failed to unmarshal image index: %w", err)
- }
-
- log.Printf("Checking %d manifests in index for %s", len(index.Manifests), imageRef)
-
- // 4. Iterate through manifests in the index to find a compatible one
- for _, manifestDesc := range index.Manifests {
- if manifestDesc.Annotations == nil {
- continue
- }
-
- match := true
- // Check if all pod requirements are met by the manifest's annotations
- for reqKey, reqVal := range requirements {
- if manifestVal, ok := manifestDesc.Annotations[reqKey]; !ok || manifestVal != reqVal {
- match = false
- break
- }
- }
-
- if match {
- // Found a compatible image!
- finalImage := fmt.Sprintf("%s/%s@%s", registryName, repoName, manifestDesc.Digest)
- log.Printf("Found compatible image: %s", finalImage)
- return finalImage, nil
- }
- }
-
- return "", fmt.Errorf("no compatible image found for requirements: %v", requirements)
-}
-
// findMatchingNode searches the cache for a node that satisfies the pod's nodeSelector.
func (ws *WebhookServer) findMatchingNode(nodeSelector map[string]string) (*corev1.Node, error) {
if len(nodeSelector) == 0 {
@@ -246,8 +176,8 @@ func (ws *WebhookServer) findMatchingNode(nodeSelector map[string]string) (*core
}
// mutate is the core logic to look for compatibility labels and select a new image
-// mutate is the core logic of our webhook. It uses a cached state for efficiency.
func (ws *WebhookServer) mutate(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
+
// Decode the Pod from the AdmissionReview
pod := &corev1.Pod{}
if err := json.Unmarshal(ar.Request.Object.Raw, pod); err != nil {
@@ -273,7 +203,7 @@ func (ws *WebhookServer) mutate(ar *admissionv1.AdmissionReview) *admissionv1.Ad
targetRef = targetRefDefault
}
- // 2. Determine the target node's labels. We either have a homogenous cluster
+ // Determine the target node's labels. We either have a homogenous cluster
// (all nodes are the same) or we have to use a node selector for the image.
var nodeLabels map[string]string
if len(pod.Spec.NodeSelector) > 0 {
@@ -292,24 +222,24 @@ func (ws *WebhookServer) mutate(ar *admissionv1.AdmissionReview) *admissionv1.Ad
nodeLabels = commonLabels
}
- // 3. Download and parse the compatibility spec from the OCI registry.
+ // Download and parse the compatibility spec from the OCI registry.
ctx := context.Background()
// Download the artifact (compatibility spec) from the uri
- // TODO we should have mode to cache these and not need to re-download
+ // TODO (vsoch) we should have mode to cache these and not need to re-download
spec, err := artifact.DownloadCompatibilityArtifact(ctx, imageRef)
if err != nil {
return deny(ar, fmt.Sprintf("compatibility spec %s issue: %v", imageRef, err))
}
- // 4. Evaluate the spec against the node's labels to find the winning tag.
+ // Evaluate the spec against the node's labels to find the winning tag.
// The "tag" attribute we are hijacking here to put the full container URI
finalImage, err := validator.EvaluateCompatibilitySpec(spec, nodeLabels)
if err != nil {
return deny(ar, fmt.Sprintf("failed to find compatible image: %v", err))
}
- // 6. Create and apply the JSON patch (this logic is unchanged).
+ // Create and apply the JSON patch
var patches []JSONPatch
containerFound := false
for i, c := range pod.Spec.Containers {
@@ -375,7 +305,7 @@ func (ws *WebhookServer) handleMutate(w http.ResponseWriter, r *http.Request) {
func main() {
- // --- Kubernetes Client and Informer Setup ---
+ // Kubernetes Client and Informer Setup
// We want to have a view of cluster nodes via NFD
config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))
if err != nil {
@@ -405,7 +335,7 @@ func main() {
ws.recalculateHomogeneity()
},
UpdateFunc: func(oldObj, newObj interface{}) {
- // Optimization: only recalculate if compatibility labels have changed.
+ // Only recalculate if compatibility labels have changed.
oldNode := oldObj.(*corev1.Node)
newNode := newObj.(*corev1.Node)
if !reflect.DeepEqual(getCompatibilityLabels(oldNode), getCompatibilityLabels(newNode)) {
@@ -414,13 +344,13 @@ func main() {
},
})
- // Start informer and wait for cache sync (same as before)
+ // Start informer and wait for cache sync
go factory.Start(stopCh)
if !cache.WaitForCacheSync(stopCh, nodeInformer.HasSynced) {
log.Fatal("failed to wait for caches to sync")
}
- // --- NEW: Perform the initial calculation after cache sync ---
+ // Perform the initial calculation after cache sync
log.Println("Performing initial cluster homogeneity check...")
ws.recalculateHomogeneity()
@@ -446,7 +376,7 @@ func main() {
}
}()
- // Graceful shutdown
+ // Graceful (or not so graceful) shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
diff --git a/docs/ocifit-k8s-v1.png b/docs/ocifit-k8s-v1.png
new file mode 100644
index 0000000..31043f9
Binary files /dev/null and b/docs/ocifit-k8s-v1.png differ
diff --git a/docs/ocifit-k8s.png b/docs/ocifit-k8s.png
index 31043f9..19b2926 100644
Binary files a/docs/ocifit-k8s.png and b/docs/ocifit-k8s.png differ
diff --git a/pkg/artifact/artifact.go b/pkg/artifact/artifact.go
index 24f41e0..7ef25d0 100644
--- a/pkg/artifact/artifact.go
+++ b/pkg/artifact/artifact.go
@@ -17,7 +17,7 @@ import (
func DownloadCompatibilityArtifact(ctx context.Context, specRef string) (*types.CompatibilitySpec, error) {
log.Printf("Downloading compatibility spec from: %s", specRef)
- // --- Step 1: Connect to the registry and resolve the manifest by its tag ---
+ // 1. Connect to the registry and resolve the manifest by its tag
reg, err := remote.NewRegistry(strings.Split(specRef, "/")[0])
if err != nil {
return nil, fmt.Errorf("failed to connect to registry for spec: %w", err)
@@ -31,7 +31,7 @@ func DownloadCompatibilityArtifact(ctx context.Context, specRef string) (*types.
return nil, fmt.Errorf("failed to resolve spec manifest %s: %w", specRef, err)
}
- // --- Step 2: Fetch and parse the OCI Manifest itself ---
+ // 2. Fetch and parse the OCI Manifest itself
log.Println("Fetching OCI manifest content...")
manifestBytes, err := content.FetchAll(ctx, repo, manifestDesc)
if err != nil {
@@ -43,7 +43,7 @@ func DownloadCompatibilityArtifact(ctx context.Context, specRef string) (*types.
}
log.Printf("Successfully parsed OCI manifest (artifact type: %s)", manifest.ArtifactType)
- // --- Step 3: Find the correct layer within the manifest ---
+ // 3. Find the correct layer within the manifest
log.Printf("Searching for spec layer with media type: %s", types.CompatibilitySpecMediaType)
var specLayerDesc *ocispec.Descriptor
for _, layer := range manifest.Layers {
@@ -59,14 +59,14 @@ func DownloadCompatibilityArtifact(ctx context.Context, specRef string) (*types.
}
log.Printf("Found spec layer with digest: %s", specLayerDesc.Digest)
- // --- Step 4: Fetch the content of the spec layer using its descriptor ---
+ // 4. Fetch the content of the spec layer using its descriptor
log.Println("Fetching compatibility spec content...")
specBytes, err := content.FetchAll(ctx, repo, *specLayerDesc)
if err != nil {
return nil, fmt.Errorf("failed to fetch spec layer content: %w", err)
}
- // --- Step 5: Unmarshal the final spec JSON into our struct ---
+ // 5. Unmarshal the final spec JSON into our struct
var spec types.CompatibilitySpec
err = json.Unmarshal(specBytes, &spec)
if err != nil {
@@ -75,5 +75,4 @@ func DownloadCompatibilityArtifact(ctx context.Context, specRef string) (*types.
log.Printf("Successfully downloaded and parsed spec version %s", spec.Version)
return &spec, nil
-
}
diff --git a/pkg/types/types.go b/pkg/types/types.go
index 42ffdc6..20a7512 100644
--- a/pkg/types/types.go
+++ b/pkg/types/types.go
@@ -1,13 +1,12 @@
package types
-// --- Image Compatibility Spec Structs (from NFD)
+// Image Compatibility Spec Structs (from NFD)
// https://github.com/kubernetes-sigs/node-feature-discovery/blob/master/api/image-compatibility/v1alpha1/spec.go
const CompatibilitySpecMediaType = "application/vnd.oci.image.compatibilities.v1+json"
// GroupRule is a list of node feature rules.
type GroupRule struct {
- // CORRECTED: This is a slice to match the JSON array `[ ... ]`
MatchFeatures []FeatureMatcher `json:"matchFeatures"`
}
@@ -37,8 +36,6 @@ type MatchExpression struct {
Value []string `json:"value,omitempty"`
}
-// --- Image Compatibility Spec Structs ---
-
// CompatibilitySpec represents image compatibility metadata.
type CompatibilitySpec struct {
Version string `json:"version"`
diff --git a/pkg/validator/rule.go b/pkg/validator/rule.go
index 0ff78fd..4ad22ac 100644
--- a/pkg/validator/rule.go
+++ b/pkg/validator/rule.go
@@ -10,7 +10,7 @@ import (
)
func evaluateRule(expression types.MatchExpression, nodeLabels map[string]string) bool {
- // Get the value of the label from the node. The 'ok' boolean is crucial.
+ // Get the value of the label from the node
fmt.Printf("Checking expression %s against node labels %s", expression, nodeLabels)
nodeVal, ok := nodeLabels[expression.Key]
@@ -33,25 +33,33 @@ func evaluateRule(expression types.MatchExpression, nodeLabels map[string]string
return true // Found a match.
}
}
- return false // Did not find a match in the list.
+ // If we get here, we did not find a match in the list.
+ return false
case types.MatchOpNotIn:
// Rule matches if the key does not exist, OR if it exists but its value
- // is NOT in the spec's value list.
+ // is not in the spec's value list.
if !ok {
- return true // Key doesn't exist, so it can't be "In" the forbidden set.
+
+ // Key doesn't exist, so it can't be "In" the forbidden set.
+ return true
}
for _, v := range expression.Value {
+
+ // Found a forbidden match.
if nodeVal == v {
- return false // Found a forbidden match.
+ return false
}
}
- return true // Did not find any forbidden values.
+ // Did not find any forbidden values.
+ return true
case types.MatchOpInRegexp:
// Rule matches if the key exists AND its value matches any of the provided regex patterns.
if !ok {
- return false // Key must exist to match a regex.
+
+ // Key must exist to match a regex.
+ return false
}
for _, pattern := range expression.Value {
re, err := regexp.Compile(pattern)
@@ -61,16 +69,20 @@ func evaluateRule(expression types.MatchExpression, nodeLabels map[string]string
log.Printf("WARN: Invalid regexp in compatibility spec: '%s'. Error: %v", pattern, err)
continue
}
+ // Found a regex match.
if re.MatchString(nodeVal) {
- return true // Found a regex match.
+ return true
}
}
- return false // No patterns matched.
+ // No patterns matched.
+ return false
case types.MatchOpGt, types.MatchOpLt:
// Rule matches if the key exists AND its integer value is > or < the spec's value.
if !ok {
- return false // Key must exist for comparison.
+
+ // Key must exist for comparison.
+ return false
}
if len(expression.Value) == 0 {
log.Printf("WARN: Gt/Lt operator used with no value in spec for key '%s'", expression.Key)
diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go
index 8811c99..6fd04c1 100644
--- a/pkg/validator/validator.go
+++ b/pkg/validator/validator.go
@@ -7,19 +7,17 @@ import (
"ghcr.io/compspec/ocifit-k8s/pkg/types"
)
-// evaluateCompatibilitySpec finds the best matching compatibility set from the spec
-// by evaluating its rules against the node's labels.
// evaluateCompatibilitySpec finds the best matching compatibility set from the spec
// by evaluating its rules against the node's labels.
func EvaluateCompatibilitySpec(spec *types.CompatibilitySpec, nodeLabels map[string]string) (string, error) {
var bestMatch *types.Compatibility
maxWeight := -1
- // Loop through each top-level 'Compatibility' set in the spec.
+ // Loop through each top-level Compatibility set in the spec.
for i, comp := range spec.Compatibilities {
allRulesMatch := true
- // Each 'Compatibility' set can have multiple 'GroupRule's.
+ // Each Compatibility set can have multiple GroupRules
// All GroupRules must match for the set to be considered a match (AND logic).
for _, groupRule := range comp.Rules {
if !allRulesMatch {
@@ -34,7 +32,7 @@ func EvaluateCompatibilitySpec(spec *types.CompatibilitySpec, nodeLabels map[str
// All MatchExpressions must match (AND logic).
for _, expression := range featureMatcher.MatchExpressions {
if !evaluateRule(expression, nodeLabels) {
- // As soon as one expression fails, the entire 'Compatibility' set is invalid.
+ // As soon as one expression fails, the entire Compatibility set is invalid.
allRulesMatch = false
break
}
@@ -42,7 +40,7 @@ func EvaluateCompatibilitySpec(spec *types.CompatibilitySpec, nodeLabels map[str
}
}
- // If, after checking all the rules for this 'Compatibility' set, allRulesMatch is still true...
+ // If after checking all the rules for this set, allRulesMatch is still true...
if allRulesMatch {
log.Printf("Found a matching compatibility set: '%s' (weight %d)", comp.Tag, comp.Weight)
@@ -50,6 +48,7 @@ func EvaluateCompatibilitySpec(spec *types.CompatibilitySpec, nodeLabels map[str
if comp.Weight > maxWeight {
maxWeight = comp.Weight
// Capture the pointer to the current item in the slice.
+ // Note I haven't thought of how I'd want to use weight yet
bestMatch = &spec.Compatibilities[i]
}
}