diff --git a/examples/leaf/wiring/main.go b/examples/leaf/wiring/main.go index 7485d380..4eaf84c3 100644 --- a/examples/leaf/wiring/main.go +++ b/examples/leaf/wiring/main.go @@ -34,5 +34,6 @@ func main() { specs.Xtrace_Logger, specs.OT_Logger, specs.Govector, + specs.Kubernetes, ) } diff --git a/plugins/dockercompose/dockergen/dockercompose.go b/plugins/dockercompose/dockergen/dockercompose.go index eea73b79..898f408b 100644 --- a/plugins/dockercompose/dockergen/dockercompose.go +++ b/plugins/dockercompose/dockergen/dockercompose.go @@ -31,7 +31,6 @@ type instance struct { Ports map[string]uint16 // Map from bindconfig name to internal port Expose map[uint16]struct{} // Ports exposed with expose directive Config map[string]string // Map from environment variable name to value - Passthrough map[string]struct{} // Environment variables that just get passed through to the container } func NewDockerComposeFile(workspaceName, workspaceDir, fileName string) *DockerComposeFile { @@ -140,7 +139,6 @@ func (d *DockerComposeFile) addInstance(instanceName string, image string, conta Expose: make(map[uint16]struct{}), Ports: make(map[string]uint16), Config: make(map[string]string), - Passthrough: make(map[string]struct{}), } d.Instances[instanceName] = &instance return nil diff --git a/plugins/kubernetes/ir.go b/plugins/kubernetes/ir.go new file mode 100644 index 00000000..13329ca1 --- /dev/null +++ b/plugins/kubernetes/ir.go @@ -0,0 +1,32 @@ +package kubernetes + +import "github.com/blueprint-uservices/blueprint/blueprint/pkg/ir" + +// An IRNode representing a Kubernetes applicaiton deployment which is a collection of Kubernetes Pod + Service Deployment instances. +type Application struct { + AppName string + Nodes []ir.IRNode + Edges []ir.IRNode +} + +// Implements IRNode +func (n *Application) Name() string { + return n.AppName +} + +// Implements IRNode +func (n *Application) String() string { + return ir.PrettyPrintNamespace(n.AppName, "KubeApp", n.Edges, n.Nodes) +} + +// Implements ir.ArtifactGenerator +func (n *Application) GenerateArtifacts(dir string) error { + nodes := ir.Filter[ir.ArtifactGenerator](n.Nodes) + for _, node := range nodes { + err := node.GenerateArtifacts(dir) + if err != nil { + return err + } + } + return nil +} diff --git a/plugins/kubernetes/kubepod/deploy.go b/plugins/kubernetes/kubepod/deploy.go new file mode 100644 index 00000000..2114768b --- /dev/null +++ b/plugins/kubernetes/kubepod/deploy.go @@ -0,0 +1,218 @@ +package kubepod + +import ( + "fmt" + "path/filepath" + "reflect" + + "github.com/blueprint-uservices/blueprint/blueprint/pkg/blueprint" + "github.com/blueprint-uservices/blueprint/blueprint/pkg/blueprint/ioutil" + "github.com/blueprint-uservices/blueprint/blueprint/pkg/coreplugins/address" + "github.com/blueprint-uservices/blueprint/blueprint/pkg/ir" + "github.com/blueprint-uservices/blueprint/plugins/docker" + "golang.org/x/exp/slog" + + "github.com/blueprint-uservices/blueprint/plugins/kubernetes/kubepod/deploygen" +) + +// A Kubernetes pod deployer. It generates the pod config files on the local filesystem. +type kubePodDeployment interface { + ir.ArtifactGenerator +} + +// A workspace used when deploying a set of containers as a Kubernetes Pod +// +// Implements docker.ContainerWorkspace defined in docker/ir.go +// +// This workspace generates Pod files at the root of the output directory. +type kubeDeploymentWorkspace struct { + ir.VisitTrackerImpl + + info docker.ContainerWorkspaceInfo + + ImageDirs map[string]string + InstanceArgs map[string][]ir.IRNode // argnodes for each instance added to the workspace + + F *deploygen.KubeDeploymentFile +} + +// Implements ir.ArtifactGenerator +func (node *PodDeployment) GenerateArtifacts(dir string) error { + slog.Info(fmt.Sprintf("Generating container instances for Kubernetes Pod %s in %s", node.Name(), dir)) + workspace := NewKubePodWorkspace(node.Name(), dir) + return node.generateArtifacts(workspace) +} + +func (node *PodDeployment) generateArtifacts(workspace *kubeDeploymentWorkspace) error { + // Add all locally-built container images + for _, n := range ir.Filter[docker.ProvidesContainerImage](node.Nodes) { + if err := n.AddContainerArtifacts(workspace); err != nil { + return err + } + } + + // Add all pre-built container instances + for _, n := range ir.Filter[docker.ProvidesContainerInstance](node.Nodes) { + if err := n.AddContainerInstance(workspace); err != nil { + return err + } + } + + // Build the Kubernetes pod config files + if err := workspace.Finish(); err != nil { + return err + } + return nil +} + +func NewKubePodWorkspace(name string, dir string) *kubeDeploymentWorkspace { + return &kubeDeploymentWorkspace{ + info: docker.ContainerWorkspaceInfo{ + Path: filepath.Clean(dir), + Target: "kubedeployment", + }, + ImageDirs: make(map[string]string), + InstanceArgs: make(map[string][]ir.IRNode), + F: deploygen.NewKubeDeploymentFile(name, dir, name+"-deployment.yaml", name+"-service.yaml"), + } +} + +// Implements docker.ContainerWorkspace +func (p *kubeDeploymentWorkspace) Info() docker.ContainerWorkspaceInfo { + return p.info +} + +// Implements docker.ContainerWorkspace +func (p *kubeDeploymentWorkspace) CreateImageDir(imageName string) (string, error) { + // Only alphanumeric and underscores are allowed in a proc name + imageName = ir.CleanName(imageName) + imageDir, err := ioutil.CreateNodeDir(p.info.Path, imageName) + p.ImageDirs[imageName] = imageDir + return imageDir, err +} + +// Implements docker.ContainerWorkspace +func (p *kubeDeploymentWorkspace) DeclarePrebuiltInstance(instanceName string, image string, args ...ir.IRNode) error { + p.InstanceArgs[instanceName] = args + return p.F.AddImageInstance(instanceName, image) +} + +// Implements docker.ContainerWorkspace +func (p *kubeDeploymentWorkspace) DeclareLocalImage(instanceName string, imageDir string, args ...ir.IRNode) error { + slog.Info("Inside DeclareLocalImage") + p.InstanceArgs[instanceName] = args + // For now set image to instanceName + image := instanceName + return p.F.AddImageInstance(instanceName, image) +} + +// Implements docker.ContainerWorkspace +func (p *kubeDeploymentWorkspace) SetEnvironmentVariable(instanceName string, key string, val string) error { + return p.F.AddEnvVar(instanceName, key, val) +} + +// Generates the pod config file +func (p *kubeDeploymentWorkspace) Finish() error { + // We didn't set any arguments or environment variables while accumulating instances. Do so now. + if err := p.processArgNodes(); err != nil { + return err + } + + return p.F.Generate() +} + +func asMap[T any](s []*T) map[*T]struct{} { + m := make(map[*T]struct{}) + for _, v := range s { + m[v] = struct{}{} + } + return m +} + +// Goes through each container's arg nodes, determining which need to be passed to the container +// as environment variables. +// +// Has special handling for addresses; containers that bind a server will have ports assigned, +// and containers that dial to a server within this namespace will have the dial address set. +// +// We don't pick external-facing ports for any addresses; these will be set by the caller or user. +func (p *kubeDeploymentWorkspace) processArgNodes() error { + + // (1) Assign ports to containers + // Servers like backends will already be pre-bound to specific ports. Other servers + // like gRPC ones will need a port assigned. + // The networking address space in a pod is shared between containers, so port assignments + // must be unique across all containers in the pod. + var allBinds []*address.BindConfig + var assignedBinds map[*address.BindConfig]struct{} + var localAddresses map[string]string + { + localAddresses = make(map[string]string) + for _, instanceArgs := range p.InstanceArgs { + allBinds = append(allBinds, ir.Filter[*address.BindConfig](instanceArgs)...) + } + + _, assigned, err := address.AssignPorts(allBinds) + if err != nil { + return err + } + assignedBinds = asMap(assigned) + + localAddresses = make(map[string]string) + for _, bind := range allBinds { + localAddresses[bind.AddressName] = fmt.Sprintf("localhost:%v", bind.Port) + } + } + + // (2) Set environment variables for containers + // (3) Expose container ports externally + for instanceName, instanceArgs := range p.InstanceArgs { + binds, dials, remaining := address.Split(instanceArgs) + + // Handle the instanceArgs that are regular config args and not address related + for _, arg := range remaining { + switch node := arg.(type) { + case ir.IRConfig: + if node.HasValue() { + // Ignore if the value is already set, because it implies it's hard-coded + // inside the container image + } else { + // TODO: if Kubernetes supports pass-through environment variables, then + // implement this + return blueprint.Errorf("kubernetes doesn't support runtime environment variable passthrough for %v", node.Name()) + } + default: + return blueprint.Errorf("container instance %v can only accept IRConfig nodes as arguments, but found %v of type %v", instanceName, arg, reflect.TypeOf(arg)) + } + } + + // If we assigned ports for this container, then set the environment variables for them + for _, bind := range binds { + if _, isAssigned := assignedBinds[bind]; isAssigned { + p.F.AddEnvVar(instanceName, bind.Name(), fmt.Sprintf("0.0.0.0:%v", bind.Port)) + } + } + + // Expose all ports for this container + for _, bind := range binds { + p.F.ExposePort(instanceName, bind.AddressName, bind.Port) + } + + // If a dial is local, then it just calls localhost:port. If it's not local + // then ?????????????????? + for _, dial := range dials { + if addr, isLocalDial := localAddresses[dial.AddressName]; isLocalDial { + p.F.AddEnvVar(instanceName, dial.Name(), addr) + } else { + // TODO: do we pass through an environment variable? how do we know the name to dial for + // services that are outside this service? maybe just use the + // service_a.grpc.dial_addr name itself as the service lookup name??? + } + } + } + + return nil +} + +func (p *kubeDeploymentWorkspace) ImplementsBuildContext() {} +func (p *kubeDeploymentWorkspace) ImplementsContainerWorkspace() {} diff --git a/plugins/kubernetes/kubepod/deploygen/deployfile.go b/plugins/kubernetes/kubepod/deploygen/deployfile.go new file mode 100644 index 00000000..c6dfcfb4 --- /dev/null +++ b/plugins/kubernetes/kubepod/deploygen/deployfile.go @@ -0,0 +1,161 @@ +package deploygen + +import ( + "fmt" + "path/filepath" + + "github.com/blueprint-uservices/blueprint/blueprint/pkg/blueprint" + "github.com/blueprint-uservices/blueprint/blueprint/pkg/ir" + "github.com/blueprint-uservices/blueprint/plugins/kubernetes/kubetemplate" + "github.com/blueprint-uservices/blueprint/plugins/linux" + "golang.org/x/exp/slog" +) + +type KubeDeploymentFile struct { + Name string + WorkspaceDir string + FileName string + ServiceFilename string + FilePath string + NumReplicas int64 + Instances map[string]*instance +} + +type instance struct { + InstanceName string + Image string + Ports map[string]uint16 + Config map[string]string +} + +func NewKubeDeploymentFile(workspaceName string, workspaceDir string, filename string, serviceFilename string) *KubeDeploymentFile { + return &KubeDeploymentFile{ + Name: workspaceName, + WorkspaceDir: workspaceDir, + FileName: filename, + ServiceFilename: serviceFilename, + FilePath: filepath.Join(workspaceDir, filename), + Instances: make(map[string]*instance), + // For now NumReplicas is fixed. + NumReplicas: 1, + } +} + +func (k *KubeDeploymentFile) Generate() error { + slog.Info(fmt.Sprintf("Generating %v/%v", k.Name, k.FileName)) + err := kubetemplate.ExecuteTemplateToFile("kubedeployment", kubernetesTemplate, k, k.FilePath) + if err != nil { + return err + } + slog.Info("NUmber of instances: ", "num", len(k.Instances)) + serviceFilePath := filepath.Join(k.WorkspaceDir, k.ServiceFilename) + slog.Info(fmt.Sprintf("Generating %v/%v", k.Name, k.ServiceFilename)) + return kubetemplate.ExecuteTemplateToFile("kubedeployment", kubernetesServiceTemplate, k, serviceFilePath) +} + +func (k *KubeDeploymentFile) AddImageInstance(instanceName string, image string) error { + return k.addInstance(instanceName, image) +} + +func (k *KubeDeploymentFile) getInstance(instanceName string) (*instance, error) { + instanceName = ir.CleanName(instanceName) + if i, exists := k.Instances[instanceName]; exists { + return i, nil + } else { + return nil, blueprint.Errorf("container instance with name %v not found", instanceName) + } +} + +func (k *KubeDeploymentFile) AddEnvVar(instanceName string, key string, val string) error { + key = linux.EnvVar(key) + instance, err := k.getInstance(instanceName) + if err != nil { + return err + } else { + instance.Config[key] = val + return nil + } +} + +func (k *KubeDeploymentFile) ExposePort(instanceName string, portName string, port uint16) error { + instance, err := k.getInstance(instanceName) + if err != nil { + return err + } else { + instance.Ports[portName] = port + return nil + } +} + +func (k *KubeDeploymentFile) addInstance(instanceName string, image string) error { + instanceName = ir.CleanName(instanceName) + if _, exists := k.Instances[instanceName]; exists { + return blueprint.Errorf("re-declaration of container instance %v of image %v", instanceName, image) + } + instance := &instance{ + InstanceName: instanceName, + Image: image, + Ports: make(map[string]uint16), + Config: make(map[string]string), + } + k.Instances[instanceName] = instance + return nil +} + +var kubernetesTemplate = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.Name}} + labels: + blueprint.service: {{.Name}} +spec: + replicas: {{.NumReplicas}} + selector: + matchLabels: + blueprint.service: {{.Name}} + template: + metadata: + name: {{.Name}} + labels: + blueprint.service: {{.Name}} + spec: + containers: + {{range $_, $decl := .Instances}} + - name: {{.InstanceName}} + image: {{.Image}} + {{- if .Config}} + env: + {{- range $name, $value := .Config}} + - name: {{$name}} + value: "{{$value}}" + {{- end}} + {{- end}} + {{- if .Ports}} + ports: + {{- range $name, $port := .Ports}} + - containerPort: {{$port}} + {{- end}} + {{- end}} + {{- end}} + restartPolicy: Always + hostname: {{.Name}} +` + +var kubernetesServiceTemplate = ` +apiVersion: v1 +kind: Service +metadata: + name: {{.Name}} +spec: + selector: + blueprint.service: {{.Name}} + ports: + {{range $_, $decl := .Instances}} + {{- range $name, $port := .Ports}} + - name: {{$name}} + port: {{$port}} + targetPort: {{$port}} + {{- end}} + {{- end}} +` diff --git a/plugins/kubernetes/kubepod/ir.go b/plugins/kubernetes/kubepod/ir.go new file mode 100644 index 00000000..a8934b27 --- /dev/null +++ b/plugins/kubernetes/kubepod/ir.go @@ -0,0 +1,42 @@ +package kubepod + +import ( + "github.com/blueprint-uservices/blueprint/blueprint/pkg/ir" + "github.com/blueprint-uservices/blueprint/plugins/docker" +) + +// An IRNode representing a Kubernetes pod, which is simply a collection of container instances. +type PodDeployment struct { + kubePodDeployment + PodName string + Nodes []ir.IRNode + Edges []ir.IRNode +} + +// Implements IRNode +func (node *PodDeployment) Name() string { + return node.PodName +} + +// Implements IRNode +func (node *PodDeployment) String() string { + return ir.PrettyPrintNamespace(node.PodName, "KubernetesPod", node.Edges, node.Nodes) +} + +// Implements [wiring.NamespaceHandler] +func (pod *PodDeployment) Accepts(nodeType any) bool { + _, isDockerContainerNode := nodeType.(docker.Container) + return isDockerContainerNode +} + +// Implements [wiring.NamespaceHandler] +func (pod *PodDeployment) AddEdge(name string, edge ir.IRNode) error { + pod.Edges = append(pod.Edges, edge) + return nil +} + +// Implements [wiring.NamespaceHandler] +func (pod *PodDeployment) AddNode(name string, node ir.IRNode) error { + pod.Nodes = append(pod.Nodes, node) + return nil +} diff --git a/plugins/kubernetes/kubepod/wiring.go b/plugins/kubernetes/kubepod/wiring.go new file mode 100644 index 00000000..94e86aee --- /dev/null +++ b/plugins/kubernetes/kubepod/wiring.go @@ -0,0 +1,63 @@ +// Package kubepod is a plugin for instantiating multiple container instances in a single Kubernetes pod deployment. +// +// # Wiring Spec Usage +// +// To use the kubepod plugin in your wiring spec, you can declare a Kubernetes Pod Deployment, giving it a name and specifying which container instances to include +// +// kubepod.NewKubePod(spec, "my_pod", "my_container_1", "my_container_2") +// +// You can add containers to existing pods: +// +// kubepod.AddContainerToPod(spec, "my_pod", "my_container_3") +// +// To deploy an application-level service in a Kubernetes Pod, make sure you first deploy the service to a process (with the [goproc] plugin) and to a container image (with the [linuxcontainer] plugin) +// +// # Artifacts Generated +// +// During compilation, the plugin generates a `podName-deployment.yaml` file that instantiates the pod as a Kubernetes deployment and a `podName-service.yaml` file that converts the deployed pod into a Kubernetes service. +// +// # Running Artifacts +// +// You need to have a working kubernetes cluster and `kubectl` installed. +// To deploy the pods to the cluster, use the following commands: +// +// kubectl apply -f podName-deployment.yaml +// kubectl apply -f podName-service.yaml +// +// [linuxcontainer]: https://github.com/Blueprint-uServices/blueprint/tree/main/plugins/linuxcontainer +// [goproc]: https://github.com/Blueprint-uServices/blueprint/tree/main/plugins/goproc +package kubepod + +import ( + "github.com/blueprint-uservices/blueprint/blueprint/pkg/coreplugins/namespaceutil" + "github.com/blueprint-uservices/blueprint/blueprint/pkg/ir" + "github.com/blueprint-uservices/blueprint/blueprint/pkg/wiring" +) + +// [AddContainerToPod] can be used by wiring specs to add more containers to a pod +func AddContainerToPod(spec wiring.WiringSpec, podName string, containerName string) { + namespaceutil.AddNodeTo[PodDeployment](spec, podName, containerName) +} + +// [NewKubePod] can be used by wiring specs to create a Kubernetes Pod that instantiates a single Kubernetes Pod consisting of multiple containers. +// +// Further containers can be added to the Pod by calling [AddContainerToPod]. +// +// During compilation, generates the deployment.yaml and service.yaml files for the pod. +// +// Returns podName +func NewKubePod(spec wiring.WiringSpec, podName string, containers ...string) string { + + // If any children were provided in this call, add them to the pod via a property + for _, containerName := range containers { + AddContainerToPod(spec, podName, containerName) + } + + spec.Define(podName, &PodDeployment{}, func(ns wiring.Namespace) (ir.IRNode, error) { + pod := &PodDeployment{PodName: podName} + _, err := namespaceutil.InstantiateNamespace(ns, pod) + return pod, err + }) + + return podName +} diff --git a/plugins/kubernetes/kubetemplate/template.go b/plugins/kubernetes/kubetemplate/template.go new file mode 100644 index 00000000..79c68138 --- /dev/null +++ b/plugins/kubernetes/kubetemplate/template.go @@ -0,0 +1,62 @@ +package kubetemplate + +import ( + "bytes" + "html/template" + "os" + "strings" + + "github.com/blueprint-uservices/blueprint/plugins/linux" +) + +func ExecuteTemplate(name string, body string, args any) (string, error) { + return newTemplateExecutor(args).exec(name, body, args) +} + +func ExecuteTemplateToFile(name string, body string, args any, filename string) error { + f, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, 0755) + if err != nil { + return err + } + defer f.Close() + + code, err := ExecuteTemplate(name, body, args) + if err != nil { + return err + } + _, err = f.WriteString(code) + return err +} + +type templateExecutor struct { + Funcs template.FuncMap +} + +func newTemplateExecutor(args any) *templateExecutor { + e := &templateExecutor{ + Funcs: template.FuncMap{}, + } + + e.Funcs["EnvVarName"] = e.EnvVarName + e.Funcs["Title"] = e.TitleCase + + return e +} + +func (e *templateExecutor) exec(name string, body string, args any) (string, error) { + t, err := template.New(name).Funcs(e.Funcs).Parse(body) + if err != nil { + return "", err + } + buf := &bytes.Buffer{} + err = t.Execute(buf, args) + return buf.String(), err +} + +func (e *templateExecutor) EnvVarName(name string) (string, error) { + return linux.EnvVar(name), nil +} + +func (e *templateExecutor) TitleCase(arg string) (string, error) { + return strings.Title(arg), nil +} diff --git a/plugins/kubernetes/wiring.go b/plugins/kubernetes/wiring.go new file mode 100644 index 00000000..f7ff5b82 --- /dev/null +++ b/plugins/kubernetes/wiring.go @@ -0,0 +1,95 @@ +// Package kubernetes is a plugin for instantiating multiple container instances in a Kubernetes cluster. +// +// # Wiring Spec Usage +// +// To use the kubernetes plugin in your wiring spec, you can declare a Kuberenetes application, giving it a name and specifying which containers to include. Each container will be deployed in a separate pod in the Kubernetes cluster. +// +// kubernetes.NewApplication(spec, "my_app", "my_container_1", "my_container_2") +// +// You can add more containers to an existing application: +// +// kubernetes.AddContainerToDeployment(spec, "my_app", "my_container_3") +// +// You can also deploy multiple containers in a single pod: +// +// kubernetes.AddPodToApplication(spec, "my_app", "my_container_4", "my_container_5") +// +// # Artifacts Generated +// +// During compilation, the plugin generates deployment.yaml and service.yaml files for each Pod. +// +// # Running Artifacts +// +// You need to have a working kubernetes cluster and `kubectl` installed. +// To deploy the pods to the cluster, use the following commands: +// +// kubectl apply -f podName-deployment.yaml +// kubectl apply -f podName-service.yaml +// +package kubernetes + +import ( + "github.com/blueprint-uservices/blueprint/blueprint/pkg/coreplugins/namespaceutil" + "github.com/blueprint-uservices/blueprint/blueprint/pkg/ir" + "github.com/blueprint-uservices/blueprint/blueprint/pkg/wiring" + "github.com/blueprint-uservices/blueprint/plugins/kubernetes/kubepod" +) + +// [AddContainerToApplication] can be used by wiring specs to add more containers to a Kubernetes application +func AddContainerToApplication(spec wiring.WiringSpec, appName string, containerName string) { + AddPodToApplication(spec, appName, containerName) +} + +// [AddPodToApplication] can be used by wiring specs to bundle multiple containers in a single Kubernetes Pod and add that pod to an application +func AddPodToApplication(spec wiring.WiringSpec, appName string, containers ...string) { + podName := kubepod.NewKubePod(spec, containers[0], containers...) + namespaceutil.AddNodeTo[Application](spec, appName, podName) +} + +// [NewApplication] can be used by wiring specs to create a Kubernetes Application that instantiates a number of kubernetes pod deployments as services. For each provided container, a new pod deployment is created with that container added to the pod. +// +// Further pod deployments for containers can be generated by calling [AddContainerToApplication]. +// +// If one wishes to bundle multiple containers into a single pod, then that can be done by calling [AddPodToApplication]. Note that the containers provided to that must not have already been added to the application before. +// +// During compilation, generates the various configuration files for generating pod deployments and services. +// +// Returns appName +func NewApplication(spec wiring.WiringSpec, appName string, containers ...string) string { + + // If any children were provided in this call, add them to the app via a property + for _, containerName := range containers { + AddContainerToApplication(spec, appName, containerName) + } + + spec.Define(appName, &Application{}, func(ns wiring.Namespace) (ir.IRNode, error) { + application := &Application{AppName: appName} + _, err := namespaceutil.InstantiateNamespace(ns, &applicationNamespace{application}) + return application, err + }) + + return appName +} + +// A [wiring.NamespaceHandler] used to build kubernetes deployments +type applicationNamespace struct { + *Application +} + +// Implements [wiring.NamespaceHandler] +func (application *Application) Accepts(nodeType any) bool { + _, isPodDeploymentNode := nodeType.(kubepod.PodDeployment) + return isPodDeploymentNode +} + +// Implements [wiring.NamespaceHandler] +func (application *Application) AddEdge(name string, edge ir.IRNode) error { + application.Edges = append(application.Edges, edge) + return nil +} + +// Implements [wiring.NamespaceHandler] +func (application *Application) AddNode(name string, node ir.IRNode) error { + application.Nodes = append(application.Nodes, node) + return nil +} diff --git a/plugins/linuxcontainer/deploy_docker.go b/plugins/linuxcontainer/deploy_docker.go index aebfde27..484ce923 100644 --- a/plugins/linuxcontainer/deploy_docker.go +++ b/plugins/linuxcontainer/deploy_docker.go @@ -79,6 +79,7 @@ func (node *Container) AddContainerArtifacts(target docker.ContainerWorkspace) e func (node *Container) AddContainerInstance(target docker.ContainerWorkspace) error { // The instance only needs to be added to the output directory once if target.Visited(node.InstanceName + ".instance") { + slog.Info(fmt.Sprintf("%v already declared as a container", node.InstanceName)) return nil }