Skip to content

Commit 149d19e

Browse files
authored
Merge pull request #3 from compspec/add-automated-build
ci: add automated build
2 parents 1134a5e + 9ede7a1 commit 149d19e

File tree

9 files changed

+96
-109
lines changed

9 files changed

+96
-109
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: build ocifit-k8s
2+
3+
on:
4+
5+
# Publish packages on release
6+
release:
7+
types: [published]
8+
pull_request: []
9+
10+
# On push to main we build and deploy images
11+
push:
12+
branches:
13+
- main
14+
15+
jobs:
16+
build:
17+
permissions:
18+
packages: write
19+
20+
runs-on: ubuntu-latest
21+
name: Build
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@v3
25+
26+
- name: Build Container
27+
if: (github.event_name != 'release')
28+
run: make
29+
30+
- name: GHCR Login
31+
if: (github.event_name != 'pull_request')
32+
uses: docker/login-action@v2
33+
with:
34+
registry: ghcr.io
35+
username: ${{ github.actor }}
36+
password: ${{ secrets.GITHUB_TOKEN }}
37+
38+
- name: Build Container
39+
if: (github.event_name == 'release')
40+
run: |
41+
tag=${GITHUB_REF#refs/tags/}
42+
make IMAGE_TAG=$tag
43+
make push IMAGE_TAG=$tag
44+
45+
- name: Deploy
46+
if: (github.event_name == 'push')
47+
run: make push

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2+
# OCIFit
3+
14
<p align="center">
25
<img src="docs/ocifit-k8s.png" alt="OCIFit Kubernetes">
36
</p>

cmd/ocifit/main.go

Lines changed: 13 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// main.go
21
package main
32

43
import (
@@ -19,7 +18,6 @@ import (
1918

2019
"ghcr.io/compspec/ocifit-k8s/pkg/artifact"
2120
"ghcr.io/compspec/ocifit-k8s/pkg/validator"
22-
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2321
admissionv1 "k8s.io/api/admission/v1"
2422
corev1 "k8s.io/api/core/v1"
2523
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -31,8 +29,6 @@ import (
3129
v1listers "k8s.io/client-go/listers/core/v1"
3230
"k8s.io/client-go/tools/cache"
3331
"k8s.io/client-go/tools/clientcmd"
34-
"oras.land/oras-go/v2/content"
35-
"oras.land/oras-go/v2/registry/remote"
3632
)
3733

3834
const (
@@ -108,7 +104,6 @@ func (ws *WebhookServer) recalculateHomogeneity() {
108104
// We can't include control plane nodes - they don't have NFD labels
109105
workerNodeSelector, err := labels.Parse("!node-role.kubernetes.io/control-plane")
110106
if err != nil {
111-
// This is a static string, so this failure is fatal for the controller's logic.
112107
log.Fatalf("FATAL: Failed to parse worker node selector: %v", err)
113108
}
114109

@@ -148,82 +143,17 @@ func (ws *WebhookServer) recalculateHomogeneity() {
148143
ws.commonLabels = referenceLabels
149144
}
150145

151-
// --- WebhookServer with Node Cache ---
146+
// WebhookServer with Node Cache
152147
type WebhookServer struct {
153148
nodeLister v1listers.NodeLister
154149
server *http.Server
155150

156-
// Cached state and a lock to protect it ---
151+
// Cached state and a lock to protect it
157152
stateLock sync.RWMutex
158153
isHomogenous bool
159154
commonLabels map[string]string
160155
}
161156

162-
// findCompatibleImage uses ORAS to find an image that matches the requirements
163-
func findCompatibleImage(ctx context.Context, imageRef string, requirements map[string]string) (string, error) {
164-
registryName, repoAndTag, found := strings.Cut(imageRef, "/")
165-
if !found {
166-
return "", fmt.Errorf("invalid image reference format: %s", imageRef)
167-
}
168-
repoName, tag, found := strings.Cut(repoAndTag, ":")
169-
if !found {
170-
tag = "latest" // Default tag
171-
}
172-
173-
// 1. Connect to the remote registry
174-
reg, err := remote.NewRegistry(registryName)
175-
if err != nil {
176-
return "", fmt.Errorf("failed to connect to registry %s: %w", registryName, err)
177-
}
178-
repo, err := reg.Repository(ctx, repoName)
179-
if err != nil {
180-
return "", fmt.Errorf("failed to access repository %s: %w", repoName, err)
181-
}
182-
183-
// 2. Resolve the image index descriptor by its tag
184-
indexDesc, err := repo.Resolve(ctx, tag)
185-
if err != nil {
186-
return "", fmt.Errorf("failed to resolve image index %s:%s: %w", repoName, tag, err)
187-
}
188-
189-
// 3. Fetch and unmarshal the image index
190-
indexBytes, err := content.FetchAll(ctx, repo, indexDesc)
191-
if err != nil {
192-
return "", fmt.Errorf("failed to fetch image index content: %w", err)
193-
}
194-
var index ocispec.Index
195-
if err := json.Unmarshal(indexBytes, &index); err != nil {
196-
return "", fmt.Errorf("failed to unmarshal image index: %w", err)
197-
}
198-
199-
log.Printf("Checking %d manifests in index for %s", len(index.Manifests), imageRef)
200-
201-
// 4. Iterate through manifests in the index to find a compatible one
202-
for _, manifestDesc := range index.Manifests {
203-
if manifestDesc.Annotations == nil {
204-
continue
205-
}
206-
207-
match := true
208-
// Check if all pod requirements are met by the manifest's annotations
209-
for reqKey, reqVal := range requirements {
210-
if manifestVal, ok := manifestDesc.Annotations[reqKey]; !ok || manifestVal != reqVal {
211-
match = false
212-
break
213-
}
214-
}
215-
216-
if match {
217-
// Found a compatible image!
218-
finalImage := fmt.Sprintf("%s/%s@%s", registryName, repoName, manifestDesc.Digest)
219-
log.Printf("Found compatible image: %s", finalImage)
220-
return finalImage, nil
221-
}
222-
}
223-
224-
return "", fmt.Errorf("no compatible image found for requirements: %v", requirements)
225-
}
226-
227157
// findMatchingNode searches the cache for a node that satisfies the pod's nodeSelector.
228158
func (ws *WebhookServer) findMatchingNode(nodeSelector map[string]string) (*corev1.Node, error) {
229159
if len(nodeSelector) == 0 {
@@ -246,8 +176,8 @@ func (ws *WebhookServer) findMatchingNode(nodeSelector map[string]string) (*core
246176
}
247177

248178
// mutate is the core logic to look for compatibility labels and select a new image
249-
// mutate is the core logic of our webhook. It uses a cached state for efficiency.
250179
func (ws *WebhookServer) mutate(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
180+
251181
// Decode the Pod from the AdmissionReview
252182
pod := &corev1.Pod{}
253183
if err := json.Unmarshal(ar.Request.Object.Raw, pod); err != nil {
@@ -273,7 +203,7 @@ func (ws *WebhookServer) mutate(ar *admissionv1.AdmissionReview) *admissionv1.Ad
273203
targetRef = targetRefDefault
274204
}
275205

276-
// 2. Determine the target node's labels. We either have a homogenous cluster
206+
// Determine the target node's labels. We either have a homogenous cluster
277207
// (all nodes are the same) or we have to use a node selector for the image.
278208
var nodeLabels map[string]string
279209
if len(pod.Spec.NodeSelector) > 0 {
@@ -292,24 +222,24 @@ func (ws *WebhookServer) mutate(ar *admissionv1.AdmissionReview) *admissionv1.Ad
292222
nodeLabels = commonLabels
293223
}
294224

295-
// 3. Download and parse the compatibility spec from the OCI registry.
225+
// Download and parse the compatibility spec from the OCI registry.
296226
ctx := context.Background()
297227

298228
// Download the artifact (compatibility spec) from the uri
299-
// TODO we should have mode to cache these and not need to re-download
229+
// TODO (vsoch) we should have mode to cache these and not need to re-download
300230
spec, err := artifact.DownloadCompatibilityArtifact(ctx, imageRef)
301231
if err != nil {
302232
return deny(ar, fmt.Sprintf("compatibility spec %s issue: %v", imageRef, err))
303233
}
304234

305-
// 4. Evaluate the spec against the node's labels to find the winning tag.
235+
// Evaluate the spec against the node's labels to find the winning tag.
306236
// The "tag" attribute we are hijacking here to put the full container URI
307237
finalImage, err := validator.EvaluateCompatibilitySpec(spec, nodeLabels)
308238
if err != nil {
309239
return deny(ar, fmt.Sprintf("failed to find compatible image: %v", err))
310240
}
311241

312-
// 6. Create and apply the JSON patch (this logic is unchanged).
242+
// Create and apply the JSON patch
313243
var patches []JSONPatch
314244
containerFound := false
315245
for i, c := range pod.Spec.Containers {
@@ -375,7 +305,7 @@ func (ws *WebhookServer) handleMutate(w http.ResponseWriter, r *http.Request) {
375305

376306
func main() {
377307

378-
// --- Kubernetes Client and Informer Setup ---
308+
// Kubernetes Client and Informer Setup
379309
// We want to have a view of cluster nodes via NFD
380310
config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))
381311
if err != nil {
@@ -405,7 +335,7 @@ func main() {
405335
ws.recalculateHomogeneity()
406336
},
407337
UpdateFunc: func(oldObj, newObj interface{}) {
408-
// Optimization: only recalculate if compatibility labels have changed.
338+
// Only recalculate if compatibility labels have changed.
409339
oldNode := oldObj.(*corev1.Node)
410340
newNode := newObj.(*corev1.Node)
411341
if !reflect.DeepEqual(getCompatibilityLabels(oldNode), getCompatibilityLabels(newNode)) {
@@ -414,13 +344,13 @@ func main() {
414344
},
415345
})
416346

417-
// Start informer and wait for cache sync (same as before)
347+
// Start informer and wait for cache sync
418348
go factory.Start(stopCh)
419349
if !cache.WaitForCacheSync(stopCh, nodeInformer.HasSynced) {
420350
log.Fatal("failed to wait for caches to sync")
421351
}
422352

423-
// --- NEW: Perform the initial calculation after cache sync ---
353+
// Perform the initial calculation after cache sync
424354
log.Println("Performing initial cluster homogeneity check...")
425355
ws.recalculateHomogeneity()
426356

@@ -446,7 +376,7 @@ func main() {
446376
}
447377
}()
448378

449-
// Graceful shutdown
379+
// Graceful (or not so graceful) shutdown
450380
sigCh := make(chan os.Signal, 1)
451381
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
452382
<-sigCh

docs/ocifit-k8s-v1.png

132 KB
Loading

docs/ocifit-k8s.png

175 KB
Loading

pkg/artifact/artifact.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
func DownloadCompatibilityArtifact(ctx context.Context, specRef string) (*types.CompatibilitySpec, error) {
1818
log.Printf("Downloading compatibility spec from: %s", specRef)
1919

20-
// --- Step 1: Connect to the registry and resolve the manifest by its tag ---
20+
// 1. Connect to the registry and resolve the manifest by its tag
2121
reg, err := remote.NewRegistry(strings.Split(specRef, "/")[0])
2222
if err != nil {
2323
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.
3131
return nil, fmt.Errorf("failed to resolve spec manifest %s: %w", specRef, err)
3232
}
3333

34-
// --- Step 2: Fetch and parse the OCI Manifest itself ---
34+
// 2. Fetch and parse the OCI Manifest itself
3535
log.Println("Fetching OCI manifest content...")
3636
manifestBytes, err := content.FetchAll(ctx, repo, manifestDesc)
3737
if err != nil {
@@ -43,7 +43,7 @@ func DownloadCompatibilityArtifact(ctx context.Context, specRef string) (*types.
4343
}
4444
log.Printf("Successfully parsed OCI manifest (artifact type: %s)", manifest.ArtifactType)
4545

46-
// --- Step 3: Find the correct layer within the manifest ---
46+
// 3. Find the correct layer within the manifest
4747
log.Printf("Searching for spec layer with media type: %s", types.CompatibilitySpecMediaType)
4848
var specLayerDesc *ocispec.Descriptor
4949
for _, layer := range manifest.Layers {
@@ -59,14 +59,14 @@ func DownloadCompatibilityArtifact(ctx context.Context, specRef string) (*types.
5959
}
6060
log.Printf("Found spec layer with digest: %s", specLayerDesc.Digest)
6161

62-
// --- Step 4: Fetch the content of the spec layer using its descriptor ---
62+
// 4. Fetch the content of the spec layer using its descriptor
6363
log.Println("Fetching compatibility spec content...")
6464
specBytes, err := content.FetchAll(ctx, repo, *specLayerDesc)
6565
if err != nil {
6666
return nil, fmt.Errorf("failed to fetch spec layer content: %w", err)
6767
}
6868

69-
// --- Step 5: Unmarshal the final spec JSON into our struct ---
69+
// 5. Unmarshal the final spec JSON into our struct
7070
var spec types.CompatibilitySpec
7171
err = json.Unmarshal(specBytes, &spec)
7272
if err != nil {
@@ -75,5 +75,4 @@ func DownloadCompatibilityArtifact(ctx context.Context, specRef string) (*types.
7575

7676
log.Printf("Successfully downloaded and parsed spec version %s", spec.Version)
7777
return &spec, nil
78-
7978
}

pkg/types/types.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package types
22

3-
// --- Image Compatibility Spec Structs (from NFD)
3+
// Image Compatibility Spec Structs (from NFD)
44
// https://github.com/kubernetes-sigs/node-feature-discovery/blob/master/api/image-compatibility/v1alpha1/spec.go
55

66
const CompatibilitySpecMediaType = "application/vnd.oci.image.compatibilities.v1+json"
77

88
// GroupRule is a list of node feature rules.
99
type GroupRule struct {
10-
// CORRECTED: This is a slice to match the JSON array `[ ... ]`
1110
MatchFeatures []FeatureMatcher `json:"matchFeatures"`
1211
}
1312

@@ -37,8 +36,6 @@ type MatchExpression struct {
3736
Value []string `json:"value,omitempty"`
3837
}
3938

40-
// --- Image Compatibility Spec Structs ---
41-
4239
// CompatibilitySpec represents image compatibility metadata.
4340
type CompatibilitySpec struct {
4441
Version string `json:"version"`

0 commit comments

Comments
 (0)