From e0856409cbfe1c851ca7a588856a2dc9ee08dcfe Mon Sep 17 00:00:00 2001 From: Georgi Chulkov Date: Thu, 29 Feb 2024 17:20:08 +0100 Subject: [PATCH] Move OOB console into this repo --- Dockerfile | 22 +-- Makefile | 2 +- config/rbac/role.yaml | 4 +- console/console.go | 300 ++++++++++++++++++++++++++++++++ console/k8s.go | 137 +++++++++++++++ console/main.go | 172 ++++++++++++++++++ console/terminal.go | 82 +++++++++ controllers/controllers_test.go | 2 +- controllers/ip_controller.go | 4 +- go.mod | 4 +- go.sum | 6 +- hack/setup-git-redirect.sh | 15 -- openapi/zz_generated.openapi.go | 2 +- test/ipam.ironcore.dev_ips.yaml | 124 +++++++++++++ test/ipam.onmetal.de_ips.yaml | 125 ------------- tools.go | 2 +- 16 files changed, 833 insertions(+), 170 deletions(-) create mode 100644 console/console.go create mode 100644 console/k8s.go create mode 100644 console/main.go create mode 100644 console/terminal.go delete mode 100755 hack/setup-git-redirect.sh create mode 100644 test/ipam.ironcore.dev_ips.yaml delete mode 100644 test/ipam.onmetal.de_ips.yaml diff --git a/Dockerfile b/Dockerfile index a35b5dc..edf02da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,21 @@ FROM golang:1.22 as builder ARG TARGETARCH - -WORKDIR /workspace - -ENV GOPRIVATE='github.com/onmetal/*' -COPY hack/setup-git-redirect.sh hack/ +WORKDIR /oob COPY go.mod go.mod COPY go.sum go.sum - -RUN --mount=type=ssh --mount=type=secret,id=github_pat GITHUB_PAT_PATH=/run/secrets/github_pat \ - hack/setup-git-redirect.sh && \ - mkdir -p -m 0600 ~/.ssh && \ - ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts && \ - go mod download +RUN go mod download COPY api/ api/ COPY bmc/ bmc/ +COPY console/ console/ COPY controllers/ controllers/ COPY internal/ internal/ COPY servers/ servers/ COPY *.go ./ - RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -a -o oob main.go - -RUN --mount=type=ssh --mount=type=secret,id=github_pat GITHUB_PAT_PATH=/run/secrets/github_pat go get github.com/onmetal/oob-console && go install github.com/onmetal/oob-console +RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -a -o oob-console console/main.go FROM debian:bookworm-20240211-slim @@ -38,5 +28,5 @@ RUN apt-get update && \ USER 65532:65532 ENTRYPOINT ["/oob"] -COPY --from=builder /workspace/oob . -COPY --from=builder /go/bin/oob-console . +COPY --from=builder /oob/oob . +COPY --from=builder /oob/oob-console . diff --git a/Makefile b/Makefile index 375897d..6cbd376 100644 --- a/Makefile +++ b/Makefile @@ -47,4 +47,4 @@ addlicense: ## Add license headers to all go files. .PHONY: checklicense checklicense: ## Check that every file has a license header present. - find . -name '*.go' -exec go run github.com/google/addlicense -check -c 'OnMetal authors' {} + + find . -name '*.go' -exec go run github.com/google/addlicense -check -c 'IronCore authors' {} + diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index b3ca449..b020b70 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -33,7 +33,7 @@ rules: - update - watch - apiGroups: - - ipam.onmetal.de + - ipam.ironcore.dev resources: - ips verbs: @@ -41,7 +41,7 @@ rules: - list - watch - apiGroups: - - ipam.onmetal.de + - ipam.ironcore.dev resources: - ips/status verbs: diff --git a/console/console.go b/console/console.go new file mode 100644 index 0000000..ecfdb1d --- /dev/null +++ b/console/console.go @@ -0,0 +1,300 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "io" + "net" + "os" + "os/exec" + "sync" + "syscall" + + "github.com/creack/pty" + "golang.org/x/crypto/ssh" + + "github.com/ironcore-dev/oob/internal/log" +) + +type consoleSpec struct { + typ string + host string + user string + password string + command []string +} + +type console struct { + consoleSpec + resizeImpl func() error + h int + w int +} + +func (c *console) run(ctx context.Context, in io.Reader, out io.WriteCloser) error { + if len(c.command) > 0 { + return c.runLocal(ctx, in, out, nil, nil, nil, 1) + } + + ctx = log.WithValues(ctx, "type", c.typ) + switch c.typ { + + case "ssh": + return c.runSSH(ctx, in, out, nil, nil, nil, 1) + + case "ssh-lenovo": + return c.runSSH(ctx, in, out, []byte("console 1\n"), []byte{27, '('}, []byte{'\n', 's', 'y', 's', 't', 'e', 'm', '>'}, 2) + + case "ipmi": + return c.runIPMI(ctx, in, out, nil, []byte{27, '('}, nil, 1) + + case "telnet": + return c.runTelnet(ctx, in, out, nil, []byte{94, ']'}, nil, 1) + + default: + return fmt.Errorf("unsupported console type: %s", c.typ) + } +} + +func (c *console) runLocal(ctx context.Context, in io.Reader, out io.WriteCloser, cmd, escIn, escOut []byte, escOutOrd int) error { + localcmd := exec.Command(c.command[0], c.command[1:]...) + + log.Debug(ctx, "Starting local process", "h", c.h, "w", c.w) + ptyf, err := pty.StartWithSize(localcmd, &pty.Winsize{ + Rows: uint16(c.h), + Cols: uint16(c.w), + }) + if err != nil { + return fmt.Errorf("cannot run %s: %w", c.command[0], err) + } + defer func() { _ = ptyf.Close() }() + + c.resizeImpl = func() error { + log.Debug(ctx, "Resizing PTY", "h", c.h, "w", c.w) + return pty.Setsize(ptyf, &pty.Winsize{ + Rows: uint16(c.h), + Cols: uint16(c.w), + }) + } + + closed := func() { + _ = localcmd.Process.Signal(syscall.SIGHUP) + } + err = c.start(ctx, in, out, ptyf, ptyf, closed, cmd, escIn, escOut, escOutOrd) + if err != nil { + return fmt.Errorf("error while running %s: %w", c.command[0], err) + } + + _ = localcmd.Wait() + c.resizeImpl = nil + log.Debug(ctx, "Local proccess has exited") + + return nil +} + +func (c *console) runSSH(ctx context.Context, in io.Reader, out io.WriteCloser, cmd, escIn, escOut []byte, escOutOrd int) error { + port := "22" + conf := ssh.ClientConfig{ + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + User: c.user, + Auth: []ssh.AuthMethod{ + ssh.Password(c.password), + }, + } + ctx = log.WithValues(ctx, "host", c.host, "port", port, "user", c.user) + + log.Debug(ctx, "Establishing SSH connection") + var err error + var client *ssh.Client + client, err = ssh.Dial("tcp", net.JoinHostPort(c.host, port), &conf) + if err != nil { + return fmt.Errorf("cannot connect: %w", err) + } + defer func() { + _ = client.Close() + }() + + var session *ssh.Session + session, err = client.NewSession() + if err != nil { + return fmt.Errorf("cannot create session: %w", err) + } + defer func() { + _ = session.Close() + }() + + var sshStdin io.WriteCloser + sshStdin, err = session.StdinPipe() + if err != nil { + return fmt.Errorf("cannot open stdin pipe: %w", err) + } + defer func() { + _ = sshStdin.Close() + }() + + var sshStdout io.Reader + sshStdout, err = session.StdoutPipe() + if err != nil { + return fmt.Errorf("cannot open stdout pipe: %w", err) + } + + envTerm := os.Getenv("TERM") + if envTerm == "" { + envTerm = "xterm-256color" + } + + log.Debug(ctx, "Requesting PTY", "term", envTerm, "h", c.h, "w", c.w) + err = session.RequestPty(envTerm, c.h, c.w, ssh.TerminalModes{ssh.ECHO: 0}) + if err != nil { + return fmt.Errorf("cannot request PTY: %w", err) + } + + err = session.Shell() + if err != nil { + return fmt.Errorf("cannot start remote shell: %w", err) + } + + c.resizeImpl = func() error { + log.Debug(ctx, "Resizing PTY", "h", c.h, "w", c.w) + return session.WindowChange(c.h, c.w) + } + + closed := func() { + _ = session.Close() + } + err = c.start(ctx, in, out, sshStdin, sshStdout, closed, cmd, escIn, escOut, escOutOrd) + if err != nil { + return fmt.Errorf("error while running SSH session: %w", err) + } + + _ = session.Wait() + c.resizeImpl = nil + log.Debug(ctx, "SSH session has ended") + + return nil +} + +func (c *console) runIPMI(ctx context.Context, in io.Reader, out io.WriteCloser, cmd, escIn, escOut []byte, escOutOrd int) error { + port := "623" + ctx = log.WithValues(ctx, "host", c.host, "port", port, "user", c.user) + + c.command = []string{"/usr/sbin/ipmi-console", "-h", c.host, "-u", c.user, "-p", c.password} + return c.runLocal(ctx, in, out, cmd, escIn, escOut, escOutOrd) +} + +func (c *console) runTelnet(ctx context.Context, in io.Reader, out io.WriteCloser, cmd, escIn, escOut []byte, escOutOrd int) error { + port := "23" + ctx = log.WithValues(ctx, "host", c.host, "port", port, "user", c.user) + + c.command = []string{"/usr/bin/telnet", c.host} + return c.runLocal(ctx, in, out, cmd, escIn, escOut, escOutOrd) +} + +func (c *console) start(ctx context.Context, ttyIn io.Reader, ttyOut, ptyIn io.WriteCloser, ptyOut io.Reader, closed func(), cmd, escIn, escOut []byte, escOutOrd int) error { + log.Debug(ctx, "Starting console") + if cmd != nil { + _, err := ptyIn.Write(cmd) + if err != nil { + return fmt.Errorf("cannot send initial command: %w", err) + } + } + + var closeOnce sync.Once + go func() { + _, _ = io.Copy(ptyIn, newMonitoringReader(ttyIn, escIn, 1)) + _ = ptyIn.Close() + if closed != nil { + closeOnce.Do(closed) + } + }() + go func() { + _, _ = io.Copy(ttyOut, newMonitoringReader(ptyOut, escOut, escOutOrd)) + _ = ttyOut.Close() + if closed != nil { + closeOnce.Do(closed) + } + }() + + return nil +} + +func (c *console) resize(h, w int) error { + c.h, c.w = h, w + if c.resizeImpl == nil { + return nil + } + return c.resizeImpl() +} + +type monitoringReader struct { + source io.Reader + canary []byte + ordinal int + ncanary int + nprev int +} + +func newMonitoringReader(reader io.Reader, canary []byte, ordinal int) *monitoringReader { + return &monitoringReader{source: reader, canary: canary, ordinal: ordinal, ncanary: len(canary)} +} + +func (r *monitoringReader) Read(p []byte) (int, error) { + if r.ordinal <= 0 { + return 0, io.EOF + } + + n, err := r.source.Read(p) + + if r.ncanary == 0 { + return n, err + } + + if n > 0 { + buf := p[:n] + + off := 0 + if r.nprev > 0 { + match := true + var i int + for i = 0; i < min(n, r.ncanary-r.nprev); i++ { + if buf[i] != r.canary[r.nprev+i] { + match = false + break + } + } + if match { + if i == r.ncanary-r.nprev { + r.ordinal-- + if r.ordinal == 0 { + return i, err + } + off = i + } else { + r.nprev += i + return n, err + } + } + r.nprev = 0 + } + + i := bytes.Index(buf[off:], r.canary) + for i >= 0 { + r.ordinal-- + if r.ordinal == 0 { + return off + i + r.ncanary, err + } + off += i + r.ncanary + i = bytes.Index(buf[off:], r.canary) + } + + for i = min(r.ncanary-1, n); i > 0; i-- { + if bytes.Equal(buf[n-i:], r.canary[:i]) { + r.nprev = i + break + } + } + } + + return n, err +} diff --git a/console/k8s.go b/console/k8s.go new file mode 100644 index 0000000..01ce14c --- /dev/null +++ b/console/k8s.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "fmt" + "os" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + kscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + oobv1alpha1 "github.com/ironcore-dev/oob/api/v1alpha1" + "github.com/ironcore-dev/oob/internal/log" +) + +func getInClusterNamespace() (string, error) { + ns, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + if err != nil && !os.IsNotExist(err) { + return "", fmt.Errorf("cannot determine in-cluster namespace: %w", err) + } + return string(ns), nil +} + +type k8sClients struct { + coreClient *rest.RESTClient + oobClient *rest.RESTClient + parameterCodec runtime.ParameterCodec +} + +func newK8sClients(kubeconfig string) (k8sClients, error) { + scheme := runtime.NewScheme() + utilruntime.Must(kscheme.AddToScheme(scheme)) + utilruntime.Must(oobv1alpha1.AddToScheme(scheme)) + + clients := k8sClients{ + parameterCodec: runtime.NewParameterCodec(scheme), + } + + var err error + var config *rest.Config + if kubeconfig == "" { + config, err = rest.InClusterConfig() + } else { + config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + } + if err != nil { + return k8sClients{}, fmt.Errorf("cannot configure connection to Kubernetes: %w", err) + } + config.NegotiatedSerializer = serializer.NewCodecFactory(scheme) + config.UserAgent = rest.DefaultKubernetesUserAgent() + config.ContentType = runtime.ContentTypeProtobuf + + config.APIPath = "/api" + config.GroupVersion = &corev1.SchemeGroupVersion + clients.coreClient, err = rest.RESTClientFor(config) + if err != nil { + return k8sClients{}, fmt.Errorf("cannot create core REST client: %w", err) + } + + config.APIPath = "/apis" + config.GroupVersion = &oobv1alpha1.GroupVersion + clients.oobClient, err = rest.RESTClientFor(config) + if err != nil { + return k8sClients{}, fmt.Errorf("cannot create OOB REST client: %w", err) + } + + return clients, nil +} + +func getConsoleSpec(ctx context.Context, clients k8sClients, namespace, name string) (consoleSpec, error) { + log.Debug(ctx, "Getting OOB") + var oob oobv1alpha1.OOB + err := clients.oobClient.Get().NamespaceIfScoped(namespace, namespace != "").Resource("oobs").Name(name).Do(ctx).Into(&oob) + if err != nil { + return consoleSpec{}, fmt.Errorf("cannot get OOB: %w", err) + } + if !oob.DeletionTimestamp.IsZero() { + return consoleSpec{}, fmt.Errorf("OOB is being deleted") + } + + if oob.Status.IP == "" { + return consoleSpec{}, fmt.Errorf("OOB has no IP address") + } + if oob.Status.Mac == "" { + return consoleSpec{}, fmt.Errorf("OOB has no MAC address") + } + cons := false + for _, c := range oob.Status.Capabilities { + if c == "console" { + cons = true + } + } + if !cons { + return consoleSpec{}, fmt.Errorf("OOB has no console capability") + } + if oob.Status.Console == "" { + return consoleSpec{}, fmt.Errorf("OOB had no console type") + } + + log.Debug(ctx, "Getting secret") + var secret corev1.Secret + err = clients.coreClient.Get().NamespaceIfScoped(namespace, namespace != "").Resource("secrets").Name(oob.Status.Mac).Do(ctx).Into(&secret) + if err != nil { + return consoleSpec{}, fmt.Errorf("cannot get secret: %w", err) + } + + if secret.Type != "kubernetes.io/basic-auth" { + return consoleSpec{}, fmt.Errorf("secret has incorrect type: %s", secret.Type) + } + + mac, _ := secret.Data["mac"] + if string(mac) != oob.Status.Mac { + return consoleSpec{}, fmt.Errorf("secret has incorrect MAC address") + } + + user, ok := secret.Data["username"] + if !ok { + return consoleSpec{}, fmt.Errorf("secret has no username") + } + + var passwd []byte + passwd, ok = secret.Data["password"] + if !ok { + return consoleSpec{}, fmt.Errorf("secret has no password") + } + + return consoleSpec{ + typ: oob.Status.Console, + user: string(user), + password: string(passwd), + host: oob.Status.IP, + }, nil +} diff --git a/console/main.go b/console/main.go new file mode 100644 index 0000000..1b96d9d --- /dev/null +++ b/console/main.go @@ -0,0 +1,172 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + + "github.com/go-logr/logr" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "k8s.io/klog/v2" + + "github.com/ironcore-dev/oob/internal/log" +) + +func usage() { + name := filepath.Base(os.Args[0]) + _, _ = fmt.Fprintf(os.Stderr, "Usage: %s [--option]... oob\n %s [--option]... -c command...\n %s [--option]... --connect=remote\n", name, name, name) + _, _ = fmt.Fprintf(os.Stderr, "Options:\n") + pflag.PrintDefaults() +} + +func exitUsage(err error) { + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%s: %s\n", filepath.Base(os.Args[0]), err) + } + pflag.Usage() + os.Exit(2) +} + +type params struct { + dev bool + silent bool + kubeconfig string + namespace string + oob string + command []string +} + +func parseCmdLine() params { + pflag.Usage = usage + pflag.ErrHelp = nil + pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError) + + pflag.Bool("dev", false, "Log human-readable messages at debug level.") + pflag.Bool("silent", false, "Log nothing. Overrides dev.") + pflag.String("kubeconfig", "", "Use the specified kubeconfig file.") + pflag.StringP("namespace", "n", "", "Operate in a specific namespace.") + pflag.BoolP("command", "c", false, "Run a local command instead of retrieving an OOB from Kubernetes.") + + var help bool + pflag.BoolVarP(&help, "help", "h", false, "Show this help message.") + err := viper.BindPFlags(pflag.CommandLine) + if err != nil { + exitUsage(err) + } + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() + err = pflag.CommandLine.Parse(os.Args[1:]) + if err != nil { + exitUsage(err) + } + if help { + exitUsage(nil) + } + + p := params{ + dev: viper.GetBool("dev"), + silent: viper.GetBool("silent"), + kubeconfig: viper.GetString("kubeconfig"), + namespace: viper.GetString("namespace"), + } + + if viper.GetBool("command") { + p.command = pflag.Args() + if len(p.command) == 0 { + exitUsage(fmt.Errorf("command cannot be empty")) + } + } else { + p.oob = pflag.Arg(0) + } + + if p.oob == "" && len(p.command) == 0 { + exitUsage(fmt.Errorf("an OOB, a command, or a connection must be specified")) + } + + return p +} + +func main() { + p := parseCmdLine() + + var exitCode int + defer func() { os.Exit(exitCode) }() + + ctx, stop := signal.NotifyContext(log.Setup(context.Background(), p.dev, p.silent, os.Stderr), syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGHUP) + defer stop() + log.Info(ctx, "Starting console") + + l := logr.FromContextOrDiscard(ctx) + klog.SetLogger(l) + + var c consoleSpec + var err error + if len(p.command) > 0 { + c.command = p.command + } else { + if p.oob != "" && p.namespace == "" { + p.namespace, err = getInClusterNamespace() + if err != nil { + log.Error(ctx, err) + exitCode = 1 + return + } + if p.namespace != "" { + log.Debug(ctx, "Using in-cluster namespace", "namespace", p.namespace) + } + } + + log.Debug(ctx, "Setting up Kubernetes connection") + var clients k8sClients + clients, err = newK8sClients(p.kubeconfig) + if err != nil { + log.Error(ctx, err) + exitCode = 1 + return + } + + log.Debug(ctx, "Retrieving console parameters") + c, err = getConsoleSpec(ctx, clients, p.namespace, p.oob) + if err != nil { + log.Error(ctx, err) + exitCode = 1 + return + } + } + + log.Debug(ctx, "Running in local mode") + err = runLocal(ctx, c) + if err != nil { + log.Error(ctx, err) + exitCode = 1 + return + } +} + +func runLocal(ctx context.Context, cs consoleSpec) error { + ctx = log.WithValues(ctx, "type", cs.typ, "host", cs.host, "user", cs.user) + c := console{consoleSpec: cs} + t := terminal{ + in: os.Stdin, + out: os.Stdout, + resizeFunc: c.resize, + } + + err := t.prepare(ctx) + if err != nil { + return fmt.Errorf("cannot prepare terminal: %w", err) + } + defer func() { _ = t.restore(ctx) }() + + err = c.run(ctx, os.Stdin, os.Stdout) + if err != nil { + return fmt.Errorf("error while running console: %w", err) + } + + return nil +} diff --git a/console/terminal.go b/console/terminal.go new file mode 100644 index 0000000..17c9cef --- /dev/null +++ b/console/terminal.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "golang.org/x/term" + + "github.com/ironcore-dev/oob/internal/log" +) + +type terminal struct { + in *os.File + out *os.File + state *term.State + sigs chan os.Signal + resizeFunc func(h, w int) error +} + +func (t *terminal) prepare(ctx context.Context) error { + infd := int(t.in.Fd()) + log.WithValues(ctx, "fd", infd) + + if !term.IsTerminal(infd) { + return fmt.Errorf("input file is not a terminal") + } + + log.Debug(ctx, "Switching terminal to raw mode") + var err error + t.state, err = term.MakeRaw(infd) + if err != nil { + return fmt.Errorf("cannot switch terminal to raw mode: %w", err) + } + + var h, w int + w, h, err = term.GetSize(infd) + if err != nil { + return fmt.Errorf("cannot determine size of terminal: %w", err) + } + err = t.resizeFunc(h, w) + if err != nil { + return fmt.Errorf("cannot resize console: %w", err) + } + + t.sigs = make(chan os.Signal, 1) + signal.Notify(t.sigs, syscall.SIGWINCH) + go t.handleSigs(ctx) + + return nil +} + +func (t *terminal) handleSigs(ctx context.Context) { + infd := int(t.in.Fd()) + for range t.sigs { + log.Debug(ctx, "Received SIGWINCH, resizing console") + w, h, err := term.GetSize(infd) + if err != nil { + log.Error(ctx, fmt.Errorf("cannot determine size of terminal: %w", err)) + } + err = t.resizeFunc(h, w) + if err != nil { + log.Error(ctx, fmt.Errorf("cannot resize console: %w", err)) + } + } +} + +func (t *terminal) restore(ctx context.Context) error { + signal.Stop(t.sigs) + close(t.sigs) + + // TODO: better reset, see https://man7.org/linux/man-pages/man1/tset.1.html + log.Debug(ctx, "Resetting terminal") + err := term.Restore(int(t.in.Fd()), t.state) + if err != nil { + return fmt.Errorf("cannot restore teminal to original state: %w", err) + } + + return nil +} diff --git a/controllers/controllers_test.go b/controllers/controllers_test.go index e7c2a6b..05f6232 100644 --- a/controllers/controllers_test.go +++ b/controllers/controllers_test.go @@ -64,7 +64,7 @@ var _ = BeforeSuite(func() { //+kubebuilder:scaffold:scheme testEnv := &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases"), filepath.Join("..", "test", "ipam.onmetal.de_ips.yaml")}, + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases"), filepath.Join("..", "test", "ipam.ironcore.dev_ips.yaml")}, ErrorIfCRDPathMissing: true, } var cfg *rest.Config diff --git a/controllers/ip_controller.go b/controllers/ip_controller.go index 2e8b6d0..f6d63f0 100644 --- a/controllers/ip_controller.go +++ b/controllers/ip_controller.go @@ -23,8 +23,8 @@ import ( "github.com/ironcore-dev/oob/internal/rand" ) -//+kubebuilder:rbac:groups=ipam.onmetal.de,resources=ips,verbs=get;list;watch -//+kubebuilder:rbac:groups=ipam.onmetal.de,resources=ips/status,verbs=get +//+kubebuilder:rbac:groups=ipam.ironcore.dev,resources=ips,verbs=get;list;watch +//+kubebuilder:rbac:groups=ipam.ironcore.dev,resources=ips/status,verbs=get func NewIPReconciler(namespace string, subnetLabelName string, subnetLabelValue string) (*IPReconciler, error) { return &IPReconciler{ diff --git a/go.mod b/go.mod index e08422f..23633d0 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/gorilla/mux v1.8.1 github.com/hashicorp/go-multierror v1.1.1 github.com/ironcore-dev/ipam v0.0.30 - github.com/ironcore-dev/ironcore v0.1.2-0.20240222224633-e553da0ae687 github.com/ironcore-dev/vgopath v0.1.4 github.com/onsi/ginkgo/v2 v2.15.0 github.com/onsi/gomega v1.31.1 @@ -23,6 +22,7 @@ require ( github.com/stmcginnis/gofish v0.14.0 golang.org/x/crypto v0.19.0 golang.org/x/sync v0.6.0 + golang.org/x/term v0.17.0 golang.org/x/text v0.14.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.29.2 @@ -73,6 +73,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/ironcore-dev/ironcore v0.1.2-0.20240227221311-63910df3d5cc github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -112,7 +113,6 @@ require ( golang.org/x/net v0.20.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.16.1 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 422bd28..29f15b3 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ironcore-dev/ipam v0.0.30 h1:h29tYBx4oy4m6m5UQpaGyARv92KtUvujqES2XqUYKM4= github.com/ironcore-dev/ipam v0.0.30/go.mod h1:H7B+W2PblJalf8Bl0DdXnkDGJoSQxnVEr6LW+ykflE0= -github.com/ironcore-dev/ironcore v0.1.2-0.20240222224633-e553da0ae687 h1:nskRoM0MiLw+SY0JVJyUIr2YaxivDIt+IX4k+5RJE+4= -github.com/ironcore-dev/ironcore v0.1.2-0.20240222224633-e553da0ae687/go.mod h1:SftQmDcsMKACWYvRJwlW5R4WgGW5EKzXuq+1zh0YKOA= +github.com/ironcore-dev/ironcore v0.1.2-0.20240227221311-63910df3d5cc h1:1nWPYemQo3rH6D4Hr0vtjMXjzJgmVC7NLFJx+6J9Oyc= +github.com/ironcore-dev/ironcore v0.1.2-0.20240227221311-63910df3d5cc/go.mod h1:SftQmDcsMKACWYvRJwlW5R4WgGW5EKzXuq+1zh0YKOA= github.com/ironcore-dev/vgopath v0.1.4 h1:hBMuv7+wnZp5JHkVfdg4mtP8hsIGvuv42+l+F2wmQxk= github.com/ironcore-dev/vgopath v0.1.4/go.mod h1:PTGnX8xW/QDytFR7oU4kcXr1RPDLCgAJ0ZUa5Rp8vyI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -180,8 +180,6 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= -github.com/onmetal/ipam v0.0.26 h1:1MExfeV8W24btRAxwQD41nxRxXCF3utqq9YDq4UHmS4= -github.com/onmetal/ipam v0.0.26/go.mod h1:5RWWO4gwdk1vKggVV8aVJNszNiJn6HMVr/lxMj8ON6A= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= diff --git a/hack/setup-git-redirect.sh b/hack/setup-git-redirect.sh deleted file mode 100755 index 30c11fb..0000000 --- a/hack/setup-git-redirect.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -set -e - -if [ -f "$GITHUB_PAT_PATH" ]; then - echo "Sourcing Github PAT from path" - GITHUB_PAT="$(cat "$GITHUB_PAT_PATH")" -fi - -if [ "$GITHUB_PAT" != "" ]; then - echo "Rewriting to use Github PAT" - git config --global url."https://${GITHUB_PAT}:x-oauth-basic@github.com/".insteadOf "https://github.com/" -else - echo "No Github PAT given, rewriting to use plain ssh auth" - git config --global url."git@github.com:".insteadOf "https://github.com" -fi diff --git a/openapi/zz_generated.openapi.go b/openapi/zz_generated.openapi.go index 51ba50e..787d57a 100644 --- a/openapi/zz_generated.openapi.go +++ b/openapi/zz_generated.openapi.go @@ -11719,7 +11719,7 @@ func schema_k8sio_api_core_v1_ServiceSpec(ref common.ReferenceCallback) common.O }, "externalTrafficPolicy": { SchemaProps: spec.SchemaProps{ - Description: "externalTrafficPolicy describes how nodes distribute service traffic they receive on one of the Service's \"externally-facing\" addresses (NodePorts, ExternalIPs, and LoadBalancer IPs). If set to \"Local\", the proxy will configure the service in a way that assumes that external load balancers will take care of balancing the service traffic between nodes, and so each node will deliver traffic only to the node-local endpoints of the service, without masquerading the client source IP. (Traffic mistakenly sent to a node with no endpoints will be dropped.) The default value, \"Cluster\", uses the standard behavior of routing to all endpoints evenly (possibly modified by topology and other features). Note that traffic sent to an External IP or LoadBalancer IP from within the cluster will always get \"Cluster\" semantics, but clients sending to a NodePort from within the cluster may need to take traffic policy into account when picking a node.\n\nPossible enum values:\n - `\"Cluster\"` routes traffic to all endpoints.\n - `\"Local\"`", + Description: "externalTrafficPolicy describes how nodes distribute service traffic they receive on one of the Service's \"externally-facing\" addresses (NodePorts, ExternalIPs, and LoadBalancer IPs). If set to \"Local\", the proxy will configure the service in a way that assumes that external load balancers will take care of balancing the service traffic between nodes, and so each node will deliver traffic only to the node-local endpoints of the service, without masquerading the client source IP. (Traffic mistakenly sent to a node with no endpoints will be dropped.) The default value, \"Cluster\", uses the standard behavior of routing to all endpoints evenly (possibly modified by topology and other features). Note that traffic sent to an External IP or LoadBalancer IP from within the cluster will always get \"Cluster\" semantics, but clients sending to a NodePort from within the cluster may need to take traffic policy into account when picking a node.\n\nPossible enum values:\n - `\"Cluster\"`\n - `\"Local\"`", Type: []string{"string"}, Format: "", Enum: []interface{}{"Cluster", "Local"}, diff --git a/test/ipam.ironcore.dev_ips.yaml b/test/ipam.ironcore.dev_ips.yaml new file mode 100644 index 0000000..93fba5d --- /dev/null +++ b/test/ipam.ironcore.dev_ips.yaml @@ -0,0 +1,124 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: ips.ipam.ironcore.dev +spec: + group: ipam.ironcore.dev + names: + kind: IP + listKind: IPList + plural: ips + singular: ip + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: IP Address + jsonPath: .status.reserved + name: IP + type: string + - description: Subnet + jsonPath: .spec.subnet.name + name: Subnet + type: string + - description: Consumer Group + jsonPath: .spec.consumer.apiVersion + name: Consumer Group + type: string + - description: Consumer Kind + jsonPath: .spec.consumer.kind + name: Consumer Kind + type: string + - description: Consumer Name + jsonPath: .spec.consumer.name + name: Consumer Name + type: string + - description: Processing state + jsonPath: .status.state + name: State + type: string + - description: Message + jsonPath: .status.message + name: Message + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: IP is the Schema for the ips API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: IPSpec defines the desired state of IP + properties: + consumer: + description: Consumer refers to resource IP has been booked for + properties: + apiVersion: + description: APIVersion is resource's API group + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-./a-z0-9]*[a-z0-9])?$ + type: string + kind: + description: Kind is CRD Kind for lookup + maxLength: 63 + minLength: 1 + pattern: ^[A-Z]([-A-Za-z0-9]*[A-Za-z0-9])?$ + type: string + name: + description: Name is CRD Name for lookup + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - kind + - name + type: object + ip: + description: IP allows to set desired IP address explicitly + type: string + subnet: + description: SubnetName is referring to parent subnet that holds requested + IP + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - subnet + type: object + status: + description: IPStatus defines the observed state of IP + properties: + message: + description: Message contains error details if the one has occurred + type: string + reserved: + description: Reserved is a reserved IP + type: string + state: + description: State is a network creation request processing state + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/test/ipam.onmetal.de_ips.yaml b/test/ipam.onmetal.de_ips.yaml deleted file mode 100644 index 1960f3e..0000000 --- a/test/ipam.onmetal.de_ips.yaml +++ /dev/null @@ -1,125 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.11.1 - creationTimestamp: null - name: ips.ipam.onmetal.de -spec: - group: ipam.onmetal.de - names: - kind: IP - listKind: IPList - plural: ips - singular: ip - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: IP Address - jsonPath: .status.reserved - name: IP - type: string - - description: Subnet - jsonPath: .spec.subnet.name - name: Subnet - type: string - - description: Consumer Group - jsonPath: .spec.consumer.apiVersion - name: Consumer Group - type: string - - description: Consumer Kind - jsonPath: .spec.consumer.kind - name: Consumer Kind - type: string - - description: Consumer Name - jsonPath: .spec.consumer.name - name: Consumer Name - type: string - - description: Processing state - jsonPath: .status.state - name: State - type: string - - description: Message - jsonPath: .status.message - name: Message - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: IP is the Schema for the ips API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: IPSpec defines the desired state of IP - properties: - consumer: - description: Consumer refers to resource IP has been booked for - properties: - apiVersion: - description: APIVersion is resource's API group - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-./a-z0-9]*[a-z0-9])?$ - type: string - kind: - description: Kind is CRD Kind for lookup - maxLength: 63 - minLength: 1 - pattern: ^[A-Z]([-A-Za-z0-9]*[A-Za-z0-9])?$ - type: string - name: - description: Name is CRD Name for lookup - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - required: - - kind - - name - type: object - ip: - description: IP allows to set desired IP address explicitly - type: string - subnet: - description: SubnetName is referring to parent subnet that holds requested - IP - properties: - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - type: object - x-kubernetes-map-type: atomic - required: - - subnet - type: object - status: - description: IPStatus defines the observed state of IP - properties: - message: - description: Message contains error details if the one has occurred - type: string - reserved: - description: Reserved is a reserved IP - type: string - state: - description: State is a network creation request processing state - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/tools.go b/tools.go index b8ff822..6fe74a9 100644 --- a/tools.go +++ b/tools.go @@ -7,6 +7,7 @@ package main import ( + _ "github.com/google/addlicense" _ "github.com/onsi/ginkgo/v2/ginkgo" _ "k8s.io/code-generator/cmd/applyconfiguration-gen" _ "k8s.io/code-generator/cmd/openapi-gen" @@ -14,7 +15,6 @@ import ( _ "sigs.k8s.io/controller-tools/cmd/controller-gen" _ "sigs.k8s.io/kustomize/kustomize/v4" - _ "github.com/google/addlicense" _ "github.com/ironcore-dev/ironcore/models-schema" _ "github.com/ironcore-dev/vgopath" )