Skip to content
Draft
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
4 changes: 4 additions & 0 deletions cue.mod/module.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module: "github.com/mpv/kir"
language: {
version: "v0.9.0"
}
135 changes: 135 additions & 0 deletions cueparser/cueparser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package cueparser

import (
"fmt"
"strings"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/load"
"cuelang.org/go/encoding/yaml"
)

// ProcessData processes YAML data and extracts container images from PodSpec
func ProcessData(data []byte) ([]string, error) {
// Create a CUE context
ctx := cuecontext.New()

// Load the PodSpec schema from the CUE Central Registry
bis := load.Instances([]string{"cue.dev/x/k8s.io/api/core/v1"}, nil)
if len(bis) == 0 {
return nil, fmt.Errorf("failed to load PodSpec schema from Central Registry")
}

pkgV := ctx.BuildInstance(bis[0])
if pkgV.Err() != nil {
return nil, fmt.Errorf("failed to build PodSpec schema: %v", pkgV.Err())
}

podSpec := pkgV.LookupPath(cue.ParsePath("#PodSpec"))
if podSpec.Err() != nil {
return nil, fmt.Errorf("failed to lookup PodSpec: %v", podSpec.Err())
}

// Load the YAML data
dataV, err := yaml.Extract("", data)
if err != nil {
return nil, fmt.Errorf("failed to extract YAML: %v", err)
}

dataValue := ctx.BuildFile(dataV)
if dataValue.Err() != nil {
return nil, fmt.Errorf("failed to build YAML data: %v", dataValue.Err())
}

// Unify the YAML value with the schema and validate
combined := podSpec.Unify(dataValue)
if err := combined.Validate(cue.Concrete(true)); err != nil {
return nil, fmt.Errorf("validation error: %v", err)
}

// Extract container images
var images []string

// Try to get containers from the validated PodSpec
containersValue := combined.LookupPath(cue.ParsePath("containers"))
if containersValue.Exists() {
// Iterate through containers
iter, err := containersValue.List()
if err != nil {
return nil, fmt.Errorf("failed to iterate containers: %v", err)
}

for iter.Next() {
container := iter.Value()
imageValue := container.LookupPath(cue.ParsePath("image"))
if imageValue.Exists() {
image, err := imageValue.String()
if err != nil {
return nil, fmt.Errorf("failed to get image string: %v", err)
}
images = append(images, image)
}
}
}

// Try to get initContainers from the validated PodSpec
initContainersValue := combined.LookupPath(cue.ParsePath("initContainers"))
if initContainersValue.Exists() {
// Iterate through initContainers
iter, err := initContainersValue.List()
if err != nil {
return nil, fmt.Errorf("failed to iterate initContainers: %v", err)
}

for iter.Next() {
container := iter.Value()
imageValue := container.LookupPath(cue.ParsePath("image"))
if imageValue.Exists() {
image, err := imageValue.String()
if err != nil {
return nil, fmt.Errorf("failed to get image string: %v", err)
}
images = append(images, image)
}
}
}

return images, nil
}

// ProcessKubernetesYAML processes a Kubernetes YAML document and extracts container images
func ProcessKubernetesYAML(data []byte) ([]string, error) {
// Try to extract the PodSpec directly
images, err := ProcessData(data)
if err == nil && len(images) > 0 {
return images, nil
}

// If that fails, return the error
return nil, fmt.Errorf("failed to extract PodSpec: %v", err)
}

// ProcessKubernetesListYAML processes a Kubernetes List YAML document and extracts container images
func ProcessKubernetesListYAML(data []byte) ([]string, error) {
// Split the YAML document by "---" to handle multiple resources
docs := strings.Split(string(data), "---")

var allImages []string
for _, doc := range docs {
if strings.TrimSpace(doc) == "" {
continue
}

images, err := ProcessKubernetesYAML([]byte(doc))
if err != nil {
// Log the error but continue processing other documents
fmt.Printf("Error processing document: %v\n", err)
continue
}

allImages = append(allImages, images...)
}

return allImages, nil
}
138 changes: 138 additions & 0 deletions cueparser/cueparser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package cueparser

import (
"testing"
)

func TestProcessData(t *testing.T) {
tests := []struct {
name string
data []byte
want []string
wantErr bool
}{
{
name: "Valid PodSpec with containers",
data: []byte(`
containers:
- name: test-container
image: test-image
`),
want: []string{"test-image"},
wantErr: false,
},
{
name: "Valid PodSpec with initContainers",
data: []byte(`
initContainers:
- name: init-container
image: init-image
`),
want: []string{"init-image"},
wantErr: false,
},
{
name: "Valid PodSpec with both containers and initContainers",
data: []byte(`
containers:
- name: test-container
image: test-image
initContainers:
- name: init-container
image: init-image
`),
want: []string{"test-image", "init-image"},
wantErr: false,
},
{
name: "Invalid PodSpec (missing required fields)",
data: []byte(`
containers:
- image: test-image
`),
want: nil,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ProcessData(tt.data)
if (err != nil) != tt.wantErr {
t.Errorf("ProcessData() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if len(got) != len(tt.want) {
t.Errorf("ProcessData() got %v images, want %v", len(got), len(tt.want))
return
}
for i, img := range got {
if img != tt.want[i] {
t.Errorf("ProcessData() got image %v, want %v", img, tt.want[i])
}
}
}
})
}
}

func TestProcessKubernetesListYAML(t *testing.T) {
tests := []struct {
name string
data []byte
want []string
wantErr bool
}{
{
name: "Multiple PodSpecs",
data: []byte(`
---
containers:
- name: container1
image: image1
---
containers:
- name: container2
image: image2
`),
want: []string{"image1", "image2"},
wantErr: false,
},
{
name: "Mixed valid and invalid PodSpecs",
data: []byte(`
---
containers:
- name: container1
image: image1
---
containers:
- image: image2
`),
want: []string{"image1"},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ProcessKubernetesListYAML(tt.data)
if (err != nil) != tt.wantErr {
t.Errorf("ProcessKubernetesListYAML() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if len(got) != len(tt.want) {
t.Errorf("ProcessKubernetesListYAML() got %v images, want %v", len(got), len(tt.want))
return
}
for i, img := range got {
if img != tt.want[i] {
t.Errorf("ProcessKubernetesListYAML() got image %v, want %v", img, tt.want[i])
}
}
}
})
}
}
19 changes: 16 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,38 @@ module github.com/mpv/kir
go 1.24.1

require (
cuelang.org/go v0.12.0
github.com/approvals/go-approval-tests v1.2.0
k8s.io/api v0.32.3
k8s.io/apimachinery v0.32.3
k8s.io/client-go v0.32.3
)

require (
cuelabs.dev/go/oci/ociregistry v0.0.0-20241125120445-2c00c104c6e1 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/emicklei/proto v1.13.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d // indirect
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
Expand Down
Loading
Loading