Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/go.sum linguist-generated=true
9 changes: 5 additions & 4 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ jobs:

- name: Prepare manifests for linting
run: |
go build -C deploy .
mkdir manifests
go run deploy/main.go my-images v0.0.8 vanilla > manifests/vanilla.yaml
go run deploy/main.go my-images v0.0.8 ocp > manifests/ocp.yaml
go run deploy/main.go my-images v0.0.8 vanilla my-secret > manifests/vanilla-with-secret.yaml
go run deploy/main.go my-images v0.0.8 ocp my-secret > manifests/ocp-with-secret.yaml
./deploy/deploy --k8s-flavor vanilla my-images > manifests/vanilla.yaml
./deploy/deploy --k8s-flavor ocp my-images > manifests/ocp.yaml
./deploy/deploy --k8s-flavor vanilla --secret my-secret my-images > manifests/vanilla-with-secret.yaml
./deploy/deploy --k8s-flavor ocp --secret my-secret my-images > manifests/ocp-with-secret.yaml

- name: kube-linter
uses: stackrox/kube-linter-action@v1.0.5
Expand Down
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ Talks directly to Container Runtime Interface ([CRI](https://kubernetes.io/docs/
### `image-prefetcher`

- main binary,
- meant to be run in pods of a DaemonSet,
- shipped as an OCI image,
- provides two subcommands:
- `fetch`: runs the actual image pulls via CRI, meant to run as an init container,
- `fetch`: runs the actual image pulls via CRI, meant to run as an init container
of DaemonSet pods.
Requires access to the CRI UNIX domain socket from the host.
- `sleep`: just sleeps forever, meant to run as the main container,
- `sleep`: just sleeps forever, meant to run as the main container of DaemonSet pods.

### `deploy`

Expand All @@ -29,21 +29,22 @@ Talks directly to Container Runtime Interface ([CRI](https://kubernetes.io/docs/

You can run many instances independently.

It requires a few arguments:
- **name** of the instance.
This also determines the name of a `ConfigMap` supplying names of images to fetch.
- `image-prefetcher` OCI image **version**. See [list of existing tags](https://quay.io/repository/mowsiany/image-prefetcher?tab=tags).
- **cluster flavor**. Currently one of:
It requires a single positional argument for the **name** of the instance.
This also determines the name of a `ConfigMap` supplying names of images to fetch.

It also accepts a few optional flags:
- `--version`: `image-prefetcher` OCI image tag. See [list of existing tags](https://quay.io/repository/mowsiany/image-prefetcher?tab=tags).
- `--k8s-flavor` depending on the cluster. Currently one of:
- `vanilla`: a generic Kubernetes distribution without additional restrictions.
- `ocp`: OpenShift, which requires explicitly granting special privileges.
- optional **image pull `Secret` name**. Required if the images are not pullable anonymously.
- `--secret`: image pull `Secret` name. Required if the images are not pullable anonymously.
This image pull secret should be usable for all images fetched by the given instance.
If provided, it must be of type `kubernetes.io/dockerconfigjson` and exist in the same namespace.

Example:

```
go run github.com/stackrox/image-prefetcher/deploy@main my-images v0.0.8 vanilla > manifest.yaml
go run github.com/stackrox/image-prefetcher/deploy@master my-images v0.0.8 vanilla > manifest.yaml
```

2. Prepare an image list. This should be a plain text file with one image name per line.
Expand Down
11 changes: 3 additions & 8 deletions cmd/fetch.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package cmd

import (
"log/slog"
"os"
"strings"
"time"

"github.com/stackrox/image-prefetcher/internal"
"github.com/stackrox/image-prefetcher/internal/logging"

"github.com/spf13/cobra"
)
Expand All @@ -19,11 +19,7 @@ var fetchCmd = &cobra.Command{

It talks to Container Runtime Interface API to pull images in parallel, with retries.`,
RunE: func(cmd *cobra.Command, args []string) error {
opts := &slog.HandlerOptions{AddSource: true}
if debug {
opts.Level = slog.LevelDebug
}
logger := slog.New(slog.NewTextHandler(os.Stderr, opts))
logger := logging.GetLogger()
timing := internal.TimingConfig{
ImageListTimeout: imageListTimeout,
InitialPullAttemptTimeout: initialPullAttemptTimeout,
Expand All @@ -45,7 +41,6 @@ var (
criSocket string
dockerConfigJSONPath string
imageListFile string
debug bool
imageListTimeout = time.Minute
initialPullAttemptTimeout = 30 * time.Second
maxPullAttemptTimeout = 5 * time.Minute
Expand All @@ -56,11 +51,11 @@ var (

func init() {
rootCmd.AddCommand(fetchCmd)
logging.AddFlags(fetchCmd.Flags())

fetchCmd.Flags().StringVar(&criSocket, "cri-socket", "/run/containerd/containerd.sock", "Path to CRI UNIX socket.")
fetchCmd.Flags().StringVar(&dockerConfigJSONPath, "docker-config", "", "Path to docker config json file.")
fetchCmd.Flags().StringVar(&imageListFile, "image-list-file", "", "Path to text file containing images to pull (one per line).")
fetchCmd.Flags().BoolVar(&debug, "debug", false, "Whether to enable debug logging.")

fetchCmd.Flags().DurationVar(&imageListTimeout, "image-list-timeout", imageListTimeout, "Timeout for image list calls (for debugging).")
fetchCmd.Flags().DurationVar(&initialPullAttemptTimeout, "initial-pull-attempt-timeout", initialPullAttemptTimeout, "Timeout for initial image pull call. Each subsequent attempt doubles it until max.")
Expand Down
136 changes: 68 additions & 68 deletions deploy/deployment.yaml.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ kind: RoleBinding
metadata:
name: prefetcher-privileged
subjects:
- kind: ServiceAccount
name: default
- kind: ServiceAccount
name: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
Expand Down Expand Up @@ -48,74 +48,74 @@ spec:
{{ end }}
spec:
tolerations:
# Broad toleration to match stackrox collector.
- operator: "Exists"
# Broad toleration to match stackrox collector.
- operator: "Exists"
initContainers:
- name: prefetch
image: {{ .Image }}:{{ .Version }}
args:
- "fetch"
{{ if .Secret }}
- "--docker-config=/tmp/pull-secret/.dockerconfigjson"
{{ end }}
- "--image-list-file=/tmp/list/images.txt"
{{ if .IsCRIO }}
- "--cri-socket=/tmp/cri/crio.sock"
{{ else }}
- "--cri-socket=/tmp/cri/containerd.sock"
{{ end }}
resources:
requests:
cpu: "20m"
memory: "16Mi"
limits:
cpu: "1"
memory: "256Mi"
volumeMounts:
- name: cri-socket-dir
mountPath: "/tmp/cri"
readOnly: true
- name: image-list
mountPath: "/tmp/list"
readOnly: true
{{ if .Secret }}
- mountPath: /tmp/pull-secret
name: pull-secret
readOnly: true
{{ end }}
securityContext:
readOnlyRootFilesystem: true
{{ if .NeedsPrivileged }}
allowPrivilegeEscalation: true
privileged: true
{{ end }}
containers:
- name: sleep
image: {{ .Image }}:{{ .Version }}
args:
- "sleep"
resources:
requests:
cpu: "5m"
memory: "16Mi"
limits:
cpu: "100m"
memory: "64Mi"
securityContext:
readOnlyRootFilesystem: true
volumes:
- name: prefetch
image: {{ .Image }}:{{ .Version }}
args:
- "fetch"
{{ if .Secret }}
- "--docker-config=/tmp/pull-secret/.dockerconfigjson"
{{ end }}
- "--image-list-file=/tmp/list/images.txt"
{{ if .IsCRIO }}
- "--cri-socket=/tmp/cri/crio.sock"
{{ else }}
- "--cri-socket=/tmp/cri/containerd.sock"
{{ end }}
resources:
requests:
cpu: "20m"
memory: "16Mi"
limits:
cpu: "1"
memory: "256Mi"
volumeMounts:
- name: cri-socket-dir
hostPath:
{{ if .IsCRIO }}
path: "/var/run/crio"
{{ else }}
path: "/var/run/containerd"
{{ end }}
mountPath: "/tmp/cri"
readOnly: true
- name: image-list
configMap:
name: {{ .Name }}
mountPath: "/tmp/list"
readOnly: true
{{ if .Secret }}
- name: pull-secret
secret:
secretName: {{ .Secret }}
- mountPath: /tmp/pull-secret
name: pull-secret
readOnly: true
{{ end }}
securityContext:
readOnlyRootFilesystem: true
{{ if .NeedsPrivileged }}
allowPrivilegeEscalation: true
privileged: true
{{ end }}
containers:
- name: sleep
image: {{ .Image }}:{{ .Version }}
args:
- "sleep"
resources:
requests:
cpu: "5m"
memory: "16Mi"
limits:
cpu: "100m"
memory: "64Mi"
securityContext:
readOnlyRootFilesystem: true
volumes:
- name: cri-socket-dir
hostPath:
{{ if .IsCRIO }}
path: "/var/run/crio"
{{ else }}
path: "/var/run/containerd"
{{ end }}
- name: image-list
configMap:
name: {{ .Name }}
{{ if .Secret }}
- name: pull-secret
secret:
secretName: {{ .Secret }}
{{ end }}
29 changes: 29 additions & 0 deletions deploy/flavor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import (
"fmt"
"log"
"slices"
)

type k8sFlavorType string

func (k *k8sFlavorType) UnmarshalText(text []byte) error {
if slices.Contains(allFlavors, string(text)) {
*k = k8sFlavorType(text)
return nil
}
return fmt.Errorf("unknown k8s flavor %q", text)
}

func (k *k8sFlavorType) MarshalText() (text []byte, err error) {
return []byte(*k), nil
}

func flavor(flavor string) *k8sFlavorType {
var f k8sFlavorType
if err := f.UnmarshalText([]byte(flavor)); err != nil {
log.Fatal(err)
}
return &f
}
40 changes: 29 additions & 11 deletions deploy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package main

import (
_ "embed"
"flag"
"fmt"
"log"
"os"
"strings"
"text/template"
)

Expand All @@ -16,23 +19,38 @@ type settings struct {
NeedsPrivileged bool
}

const (
vanillaFlavor = "vanilla"
ocpFlavor = "ocp"
)

var allFlavors = []string{vanillaFlavor, ocpFlavor}

const imageRepo = "quay.io/stackrox-io/image-prefetcher"

//go:embed deployment.yaml.gotpl
var daemonSetTemplate string
var deploymentTemplate string

var (
version string
k8sFlavor k8sFlavorType
secret string
)

func init() {
flag.StringVar(&version, "version", "v0.1.0", "Version of image prefetcher OCI image.")
flag.TextVar(&k8sFlavor, "k8s-flavor", flavor(vanillaFlavor), fmt.Sprintf("Kubernetes flavor. Accepted values: %s", strings.Join(allFlavors, ",")))
flag.StringVar(&secret, "secret", "", "Kubernetes image pull Secret to use when pulling.")
}

func main() {
if len(os.Args) < 4 {
println("Usage:", os.Args[0], "<name> <version> vanilla|ocp [secret]")
flag.Parse()
if len(flag.Args()) < 1 {
println("Usage:", os.Args[0], "[ FLAGS ] <name>")
os.Exit(1)
}
name := os.Args[1]
version := os.Args[2]
isOcp := os.Args[3] == "ocp"
secret := ""
if len(os.Args) > 4 {
secret = os.Args[4]
}
name := flag.Arg(0)
isOcp := k8sFlavor == ocpFlavor

s := settings{
Name: name,
Expand All @@ -42,7 +60,7 @@ func main() {
IsCRIO: isOcp,
NeedsPrivileged: isOcp,
}
tmpl := template.Must(template.New("deployment").Parse(daemonSetTemplate))
tmpl := template.Must(template.New("deployment").Parse(deploymentTemplate))
if err := tmpl.Execute(os.Stdout, s); err != nil {
log.Fatal(err)
}
Expand Down
22 changes: 22 additions & 0 deletions internal/logging/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package logging

import (
"log/slog"
"os"

"github.com/spf13/pflag"
)

var debug bool

func GetLogger() *slog.Logger {
opts := &slog.HandlerOptions{AddSource: true}
if debug {
opts.Level = slog.LevelDebug
}
return slog.New(slog.NewTextHandler(os.Stderr, opts))
}

func AddFlags(flags *pflag.FlagSet) {
flags.BoolVar(&debug, "debug", false, "Whether to enable debug logging.")
}
Loading