diff --git a/common/config/environment.go b/common/config/environment.go index ba93c41e3..4b739c350 100644 --- a/common/config/environment.go +++ b/common/config/environment.go @@ -131,8 +131,11 @@ type Env interface { CloudEnv } type CloudEnv interface { + // InternalDockerhubMirror returns the internal Dockerhub mirror. InternalDockerhubMirror() string + // InternalRegistry returns the internal registry. InternalRegistry() string + // InternalRegistryImageTagExists returns true if the image tag exists in the internal registry. InternalRegistryImageTagExists(image, tag string) (bool, error) } diff --git a/components/datadog/agentparams/params.go b/components/datadog/agentparams/params.go index 3716b50d3..ed5b877ad 100644 --- a/components/datadog/agentparams/params.go +++ b/components/datadog/agentparams/params.go @@ -234,37 +234,37 @@ func WithPulumiResourceOptions(resources ...pulumi.ResourceOption) func(*Params) } } -func withIntakeHostname(hostname pulumi.StringInput) func(*Params) error { +func withIntakeHostname(hostname pulumi.StringInput, port uint32) func(*Params) error { return func(p *Params) error { - extraConfig := pulumi.Sprintf(`dd_url: http://%[1]s:80 -logs_config.logs_dd_url: %[1]s:80 + extraConfig := pulumi.Sprintf(`dd_url: http://%[1]s:%[2]d +logs_config.logs_dd_url: %[1]s:%[2]d logs_config.logs_no_ssl: true logs_config.force_use_http: true -process_config.process_dd_url: http://%[1]s:80 -apm_config.apm_dd_url: http://%[1]s:80 -database_monitoring.metrics.logs_dd_url: %[1]s:80 +process_config.process_dd_url: http://%[1]s:%[2]d +apm_config.apm_dd_url: http://%[1]s:%[2]d +database_monitoring.metrics.logs_dd_url: %[1]s:%[2]d database_monitoring.metrics.logs_no_ssl: true -database_monitoring.activity.logs_dd_url: %[1]s:80 +database_monitoring.activity.logs_dd_url: %[1]s:%[2]d database_monitoring.activity.logs_no_ssl: true -database_monitoring.samples.logs_dd_url: %[1]s:80 +database_monitoring.samples.logs_dd_url: %[1]s:%[2]d database_monitoring.samples.logs_no_ssl: true -network_devices.metadata.logs_dd_url: %[1]s:80 +network_devices.metadata.logs_dd_url: %[1]s:%[2]d network_devices.metadata.logs_no_ssl: true -network_devices.snmp_traps.forwarder.logs_dd_url: %[1]s:80 +network_devices.snmp_traps.forwarder.logs_dd_url: %[1]s:%[2]d network_devices.snmp_traps.forwarder.logs_no_ssl: true -network_devices.netflow.forwarder.logs_dd_url: %[1]s:80 +network_devices.netflow.forwarder.logs_dd_url: %[1]s:%[2]d network_devices.netflow.forwarder.logs_no_ssl: true -network_path.forwarder.logs_dd_url: %[1]s:80 +network_path.forwarder.logs_dd_url: %[1]s:%[2]d network_path.forwarder.logs_no_ssl: true -container_lifecycle.logs_dd_url: %[1]s:80 +container_lifecycle.logs_dd_url: %[1]s:%[2]d container_lifecycle.logs_no_ssl: true -container_image.logs_dd_url: %[1]s:80 +container_image.logs_dd_url: %[1]s:%[2]d container_image.logs_no_ssl: true -sbom.logs_dd_url: %[1]s:80 +sbom.logs_dd_url: %[1]s:%[2]d sbom.logs_no_ssl: true -service_discovery.forwarder.logs_dd_url: %[1]s:80 +service_discovery.forwarder.logs_dd_url: %[1]s:%[2]d service_discovery.forwarder.logs_no_ssl: true -`, hostname) +`, hostname, port) p.ExtraAgentConfig = append(p.ExtraAgentConfig, extraConfig) return nil } @@ -276,7 +276,7 @@ service_discovery.forwarder.logs_no_ssl: true // // This option is overwritten by `WithFakeintake`. func WithIntakeHostname(hostname string) func(*Params) error { - return withIntakeHostname(pulumi.String(hostname)) + return withIntakeHostname(pulumi.String(hostname), 80) } // WithFakeintake installs the fake intake and configures the Agent to use it. @@ -285,7 +285,7 @@ func WithIntakeHostname(hostname string) func(*Params) error { func WithFakeintake(fakeintake *fakeintake.Fakeintake) func(*Params) error { return func(p *Params) error { p.ResourceOptions = append(p.ResourceOptions, utils.PulumiDependsOn(fakeintake)) - return withIntakeHostname(fakeintake.Host)(p) + return withIntakeHostname(fakeintake.Host, fakeintake.Port)(p) } } @@ -320,3 +320,11 @@ func WithTags(tags []string) func(*Params) error { return nil } } + +// WithHostname add hostname to the agent configuration +func WithHostname(hostname string) func(*Params) error { + return func(p *Params) error { + p.ExtraAgentConfig = append(p.ExtraAgentConfig, pulumi.Sprintf("hostname: %s", hostname)) + return nil + } +} diff --git a/components/remote/component.go b/components/remote/component.go index d82b72cba..aa86455e8 100644 --- a/components/remote/component.go +++ b/components/remote/component.go @@ -14,6 +14,7 @@ type HostOutput struct { CloudProvider components.CloudProviderIdentifier `json:"cloudProvider"` Address string `json:"address"` + Port int `json:"port"` Username string `json:"username"` Password string `json:"password,omitempty"` OSFamily os.Family `json:"osFamily"` @@ -28,14 +29,15 @@ type Host struct { OS os.OS - CloudProvider pulumi.StringOutput `pulumi:"cloudProvider"` Address pulumi.StringOutput `pulumi:"address"` + Port pulumi.IntOutput `pulumi:"port"` Username pulumi.StringOutput `pulumi:"username"` Password pulumi.StringOutput `pulumi:"password"` Architecture pulumi.StringOutput `pulumi:"architecture"` OSFamily pulumi.IntOutput `pulumi:"osFamily"` OSFlavor pulumi.IntOutput `pulumi:"osFlavor"` OSVersion pulumi.StringOutput `pulumi:"osVersion"` + CloudProvider pulumi.StringOutput `pulumi:"cloudProvider"` } func (h *Host) Export(ctx *pulumi.Context, out *HostOutput) error { diff --git a/components/remote/connection.go b/components/remote/connection.go index d6065db94..e26f5f248 100644 --- a/components/remote/connection.go +++ b/components/remote/connection.go @@ -15,16 +15,22 @@ const ( // NewConnection creates a remote connection to a host. // Host and user are mandatory. -func NewConnection(host pulumi.StringInput, user, sshKeyPath, sshKeyPassword, sshAgentPath string) (*remote.ConnectionArgs, error) { +func NewConnection(host pulumi.StringInput, user string, options ...ConnectionOption) (*remote.ConnectionArgs, error) { + args, err := buildConnectionArgs(host, user, options...) + + if err != nil { + return nil, err + } conn := &remote.ConnectionArgs{ - Host: host, - User: pulumi.String(user), + Host: args.host, + User: pulumi.String(args.user), PerDialTimeout: pulumi.IntPtr(dialTimeoutSeconds), DialErrorLimit: pulumi.IntPtr(dialErrorLimit), + Port: pulumi.Float64Ptr(float64(args.port)), } - if sshKeyPath != "" { - privateKey, err := utils.ReadSecretFile(sshKeyPath) + if args.privateKeyPath != "" { + privateKey, err := utils.ReadSecretFile(args.privateKeyPath) if err != nil { return nil, err } @@ -32,12 +38,12 @@ func NewConnection(host pulumi.StringInput, user, sshKeyPath, sshKeyPassword, ss conn.PrivateKey = privateKey } - if sshKeyPassword != "" { - conn.PrivateKeyPassword = pulumi.StringPtr(sshKeyPassword) + if args.privateKeyPassword != "" { + conn.PrivateKeyPassword = pulumi.StringPtr(args.privateKeyPassword) } - if sshAgentPath != "" { - conn.AgentSocketPath = pulumi.StringPtr(sshAgentPath) + if args.sshAgentPath != "" { + conn.AgentSocketPath = pulumi.StringPtr(args.sshAgentPath) } return conn, nil diff --git a/components/remote/connectionargs.go b/components/remote/connectionargs.go new file mode 100644 index 000000000..d2e324a9f --- /dev/null +++ b/components/remote/connectionargs.go @@ -0,0 +1,60 @@ +package remote + +import ( + "github.com/DataDog/test-infra-definitions/common" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +type connectionArgs struct { + host pulumi.StringInput + user string + + // ==== Optional ==== + privateKeyPath string + privateKeyPassword string + sshAgentPath string + port int +} + +type ConnectionOption = func(*connectionArgs) error + +func buildConnectionArgs(host pulumi.StringInput, user string, options ...ConnectionOption) (*connectionArgs, error) { + args := &connectionArgs{ + host: host, + user: user, + port: 22, + } + return common.ApplyOption(args, options) +} + +// WithPrivateKeyPath [optional] sets the path to the private key to use for the connection +func WithPrivateKeyPath(path string) ConnectionOption { + return func(args *connectionArgs) error { + args.privateKeyPath = path + return nil + } +} + +// WithPrivateKeyPassword [optional] sets the password to use in case the private key is encrypted +func WithPrivateKeyPassword(password string) ConnectionOption { + return func(args *connectionArgs) error { + args.privateKeyPassword = password + return nil + } +} + +// WithSSHAgentPath [optional] sets the path to the SSH Agent socket. Default to environment variable SSH_AUTH_SOCK if present. +func WithSSHAgentPath(path string) ConnectionOption { + return func(args *connectionArgs) error { + args.sshAgentPath = path + return nil + } +} + +// WithPort [optional] sets the port to use for the connection. Default to 22. +func WithPort(port int) ConnectionOption { + return func(args *connectionArgs) error { + args.port = port + return nil + } +} diff --git a/components/remote/os.go b/components/remote/os.go index fe70a524c..984e0d7c6 100644 --- a/components/remote/os.go +++ b/components/remote/os.go @@ -39,6 +39,13 @@ func InitHost(e config.Env, conn remote.ConnectionOutput, osDesc os.Descriptor, host.OSFlavor = pulumi.Int(osDesc.Flavor).ToIntOutput() host.OSVersion = pulumi.String(osDesc.Version).ToStringOutput() host.Password = password + host.Port = conn.Port().ApplyT(func(p *float64) int { + if p == nil { + // default port to 22 + return 22 + } + return int(*p) + }).(pulumi.IntOutput) // Set the OS for internal usage host.OS = os.NewOS(e, osDesc, runner) diff --git a/registry/scenarios.go b/registry/scenarios.go index 0cd245e39..0086cb52e 100644 --- a/registry/scenarios.go +++ b/registry/scenarios.go @@ -1,9 +1,10 @@ package registry import ( - "github.com/DataDog/test-infra-definitions/scenarios/gcp/gke" "strings" + "github.com/DataDog/test-infra-definitions/scenarios/gcp/gke" + "github.com/DataDog/test-infra-definitions/scenarios/aws/ec2" "github.com/DataDog/test-infra-definitions/scenarios/aws/ecs" "github.com/DataDog/test-infra-definitions/scenarios/aws/eks" @@ -13,6 +14,7 @@ import ( "github.com/DataDog/test-infra-definitions/scenarios/azure/aks" computerun "github.com/DataDog/test-infra-definitions/scenarios/azure/compute/run" gcpcompute "github.com/DataDog/test-infra-definitions/scenarios/gcp/compute/run" + localpodmanrun "github.com/DataDog/test-infra-definitions/scenarios/local/podman/run" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) @@ -21,17 +23,18 @@ type ScenarioRegistry map[string]pulumi.RunFunc func Scenarios() ScenarioRegistry { return ScenarioRegistry{ - "aws/vm": ec2.VMRun, - "aws/dockervm": ec2.VMRunWithDocker, - "aws/ecs": ecs.Run, - "aws/eks": eks.Run, - "aws/installer": installer.Run, - "aws/microvms": microvms.Run, - "aws/kind": kindvm.Run, - "az/vm": computerun.VMRun, - "az/aks": aks.Run, - "gcp/vm": gcpcompute.VMRun, - "gcp/gke": gke.Run, + "aws/vm": ec2.VMRun, + "aws/dockervm": ec2.VMRunWithDocker, + "aws/ecs": ecs.Run, + "aws/eks": eks.Run, + "aws/installer": installer.Run, + "aws/microvms": microvms.Run, + "aws/kind": kindvm.Run, + "az/vm": computerun.VMRun, + "az/aks": aks.Run, + "gcp/vm": gcpcompute.VMRun, + "gcp/gke": gke.Run, + "localpodman/vm": localpodmanrun.VMRun, } } diff --git a/resources/hyperv/vm.go b/resources/hyperv/vm.go index 2decd4fb1..94433664f 100644 --- a/resources/hyperv/vm.go +++ b/resources/hyperv/vm.go @@ -32,7 +32,12 @@ func NewVM(e Environment, args VMArgs, opts ...pulumi.ResourceOption) (*remote.H return components.NewComponent(&e, args.Name, func(comp *remote.Host) error { // Let's say you get IP address from the command output (only output in the command). - conn, err := remote.NewConnection(cmd.Stdout, "", e.DefaultPrivateKeyPath(), e.DefaultPrivateKeyPassword(), "") + conn, err := remote.NewConnection( + cmd.Stdout, + "", + remote.WithPrivateKeyPath(e.DefaultPrivateKeyPath()), + remote.WithPrivateKeyPassword(e.DefaultPrivateKeyPassword()), + ) if err != nil { return err } diff --git a/resources/local/environment.go b/resources/local/environment.go index 029169d23..b5b1eefa3 100644 --- a/resources/local/environment.go +++ b/resources/local/environment.go @@ -1,21 +1,30 @@ package local import ( - "github.com/DataDog/test-infra-definitions/common/config" + config "github.com/DataDog/test-infra-definitions/common/config" + "github.com/DataDog/test-infra-definitions/common/namer" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) +const ( + localNamerNamespace = "local" + // local Infra (local) + DDInfraDefaultPublicKeyPath = "local/defaultPublicKeyPath" +) + type Environment struct { - // CommonEnvironment is the common environment for all the components - // in the test-infra-definitions. *config.CommonEnvironment + + Namer namer.Namer } var _ config.Env = (*Environment)(nil) -// NewEnvironment creates a new local environment. func NewEnvironment(ctx *pulumi.Context) (Environment, error) { - env := Environment{} + env := Environment{ + Namer: namer.NewNamer(ctx, localNamerNamespace), + } commonEnv, err := config.NewCommonEnvironment(ctx) if err != nil { @@ -27,6 +36,8 @@ func NewEnvironment(ctx *pulumi.Context) (Environment, error) { return env, nil } +// Cross Cloud Provider config + // InternalRegistry returns the internal registry. func (e *Environment) InternalRegistry() string { return "none" @@ -41,3 +52,8 @@ func (e *Environment) InternalDockerhubMirror() string { func (e *Environment) InternalRegistryImageTagExists(_, _ string) (bool, error) { return true, nil } + +// Common +func (e *Environment) DefaultPublicKeyPath() string { + return e.InfraConfig.Get(DDInfraDefaultPublicKeyPath) +} diff --git a/resources/local/podman/data/Dockerfile b/resources/local/podman/data/Dockerfile new file mode 100644 index 000000000..470e96c26 --- /dev/null +++ b/resources/local/podman/data/Dockerfile @@ -0,0 +1,26 @@ +FROM ubuntu:20.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN set -eu && \ + apt update; \ + apt install -y \ + --no-install-recommends \ + ca-certificates \ + curl \ + systemd \ + systemd-cron \ + sudo \ + openssh-server; + +# setup ssh +RUN mkdir -p /etc/ssh/sshd_config.d && echo "PermitRootLogin without-password" > /etc/ssh/sshd_config.d/allow_root.conf +ARG DOCKER_HOST_SSH_PUBLIC_KEY +RUN mkdir -p /root/.ssh && echo $DOCKER_HOST_SSH_PUBLIC_KEY >> /root/.ssh/authorized_keys +RUN chmod 600 /root/.ssh/authorized_keys + +RUN service ssh start + +EXPOSE 22 + +CMD ["/usr/sbin/init"] \ No newline at end of file diff --git a/resources/local/podman/vm.go b/resources/local/podman/vm.go new file mode 100644 index 000000000..6461e513c --- /dev/null +++ b/resources/local/podman/vm.go @@ -0,0 +1,89 @@ +package localpodman + +import ( + _ "embed" + "os" + "path" + "runtime" + + "github.com/DataDog/test-infra-definitions/common/config" + "github.com/DataDog/test-infra-definitions/common/utils" + resourceslocal "github.com/DataDog/test-infra-definitions/resources/local" + + "github.com/pulumi/pulumi-command/sdk/go/command/local" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +type VMArgs struct { + Name string +} + +//go:embed data/Dockerfile +var dockerfileContent string + +func NewInstance(e resourceslocal.Environment, args VMArgs, opts ...pulumi.ResourceOption) (address pulumi.StringOutput, user string, port int, err error) { + interpreter := []string{"/bin/bash", "-c"} + if runtime.GOOS == "windows" { + interpreter = []string{"powershell", "-Command"} + } + + publicKey, err := os.ReadFile(e.DefaultPublicKeyPath()) + if err != nil { + return pulumi.StringOutput{}, "", -1, err + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return pulumi.StringOutput{}, "", -1, err + } + dataPath := path.Join(homeDir, ".localpodman") + // TODO clean up the folder on stack destroy + // Requires a Runner refactor to reuse crossplatform commands + err = os.MkdirAll(dataPath, 0700) + if err != nil { + return pulumi.StringOutput{}, "", -1, err + } + dockerfilePath := path.Join(dataPath, "Dockerfile") + err = os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0600) + if err != nil { + return pulumi.StringOutput{}, "", -1, err + } + + opts = utils.MergeOptions(opts, e.WithProviders(config.ProviderCommand)) + // TODO use NewLocalRunner + // requires a refactor to pass interpreter + buildPodman, err := local.NewCommand(e.Ctx(), e.CommonNamer().ResourceName("podman-build", args.Name), &local.CommandArgs{ + Interpreter: pulumi.ToStringArray(interpreter), + Environment: pulumi.StringMap{"DOCKER_HOST_SSH_PUBLIC_KEY": pulumi.String(string(publicKey))}, + Create: pulumi.Sprintf("podman build --format=docker --build-arg DOCKER_HOST_SSH_PUBLIC_KEY=\"$DOCKER_HOST_SSH_PUBLIC_KEY\" -t %s .", args.Name), + Delete: pulumi.Sprintf("podman rmi %s", args.Name), + Triggers: pulumi.Array{}, + AssetPaths: pulumi.StringArray{}, + Dir: pulumi.String(dataPath), + }, opts...) + if err != nil { + return pulumi.StringOutput{}, "", -1, err + } + opts = utils.MergeOptions(opts, pulumi.DependsOn([]pulumi.Resource{buildPodman})) + runPodman, err := local.NewCommand(e.Ctx(), e.CommonNamer().ResourceName("podman-run", args.Name), &local.CommandArgs{ + Interpreter: pulumi.ToStringArray(interpreter), + Environment: pulumi.StringMap{"DOCKER_HOST_SSH_PUBLIC_KEY": pulumi.String(string(publicKey))}, + Create: pulumi.Sprintf("podman run -d --name=%[1]s_run -p 50022:22 %[1]s", args.Name), + Delete: pulumi.Sprintf("podman stop %[1]s_run && podman rm %[1]s_run", args.Name), + Triggers: pulumi.Array{}, + AssetPaths: pulumi.StringArray{}, + Dir: pulumi.String(dataPath), + }, opts...) + if err != nil { + return pulumi.StringOutput{}, "", -1, err + } + + e.Ctx().Log.Info("Running with container of type ubuntu", nil) + + // hack to wait for the container to be up + ipAddress := runPodman.Stdout.ApplyT(func(_ string) string { + return "localhost" + }).(pulumi.StringOutput) + + return ipAddress, "root", 50022, nil +} diff --git a/scenarios/aws/ec2/vm.go b/scenarios/aws/ec2/vm.go index e7c8b52d7..e3568fffd 100644 --- a/scenarios/aws/ec2/vm.go +++ b/scenarios/aws/ec2/vm.go @@ -60,7 +60,12 @@ func NewVM(e aws.Environment, name string, params ...VMOption) (*remote.Host, er } // Create connection - conn, err := remote.NewConnection(instance.PrivateIp, sshUser, e.DefaultPrivateKeyPath(), e.DefaultPrivateKeyPassword(), "") + conn, err := remote.NewConnection( + instance.PrivateIp, + sshUser, + remote.WithPrivateKeyPath(e.DefaultPrivateKeyPath()), + remote.WithPrivateKeyPassword(e.DefaultPrivateKeyPassword()), + ) if err != nil { return err } diff --git a/scenarios/aws/microVMs/microvms/provision.go b/scenarios/aws/microVMs/microvms/provision.go index 6d8d52dbd..5fcd726e4 100644 --- a/scenarios/aws/microVMs/microvms/provision.go +++ b/scenarios/aws/microVMs/microvms/provision.go @@ -226,7 +226,12 @@ func provisionRemoteMicroVMs(vmCollections []*VMCollection, instanceEnv *Instanc } // create new ssh connection to build proxy - conn, err := remoteComp.NewConnection(collection.instance.instance.Address, "ubuntu", instanceEnv.DefaultPrivateKeyPath(), instanceEnv.DefaultPrivateKeyPassword(), "") + conn, err := remoteComp.NewConnection( + collection.instance.instance.Address, + "ubuntu", + remoteComp.WithPrivateKeyPath(instanceEnv.DefaultPrivateKeyPath()), + remoteComp.WithPrivateKeyPassword(instanceEnv.DefaultPrivateKeyPassword()), + ) if err != nil { return nil, err } @@ -285,7 +290,11 @@ func provisionLocalMicroVMs(vmCollections []*VMCollection) ([]pulumi.Resource, e } // create new ssh connection to build proxy - conn, err := remoteComp.NewConnection(domain.ip, "root", filepath.Join(GetWorkingDirectory(domain.vmset.Arch), "ddvm_rsa"), "", "") + conn, err := remoteComp.NewConnection( + domain.ip, + "root", + remoteComp.WithPrivateKeyPath(filepath.Join(GetWorkingDirectory(domain.vmset.Arch), "ddvm_rsa")), + ) if err != nil { return nil, err } diff --git a/scenarios/azure/compute/vm.go b/scenarios/azure/compute/vm.go index 12005c5a6..680ac9d52 100644 --- a/scenarios/azure/compute/vm.go +++ b/scenarios/azure/compute/vm.go @@ -58,7 +58,12 @@ func NewVM(e azure.Environment, name string, params ...VMOption) (*remote.Host, return err } - connection, err := remote.NewConnection(privateIP, compute.AdminUsername, e.DefaultPrivateKeyPath(), e.DefaultPrivateKeyPassword(), "") + connection, err := remote.NewConnection( + privateIP, + compute.AdminUsername, + remote.WithPrivateKeyPath(e.DefaultPrivateKeyPath()), + remote.WithPrivateKeyPassword(e.DefaultPrivateKeyPassword()), + ) if err != nil { return err } diff --git a/scenarios/gcp/compute/vm.go b/scenarios/gcp/compute/vm.go index 7f9417e52..561c0961c 100644 --- a/scenarios/gcp/compute/vm.go +++ b/scenarios/gcp/compute/vm.go @@ -35,7 +35,12 @@ func NewVM(e gcp.Environment, name string, option ...VMOption) (*remote.Host, er return err } - conn, err := remote.NewConnection(vm.NetworkInterfaces.Index(pulumi.Int(0)).NetworkIp().Elem(), "gce", e.DefaultPrivateKeyPath(), e.DefaultPrivateKeyPassword(), "") + conn, err := remote.NewConnection( + vm.NetworkInterfaces.Index(pulumi.Int(0)).NetworkIp().Elem(), + "gce", + remote.WithPrivateKeyPath(e.DefaultPrivateKeyPath()), + remote.WithPrivateKeyPassword(e.DefaultPrivateKeyPassword()), + ) if err != nil { return err } diff --git a/scenarios/local/podman/run/vm_run.go b/scenarios/local/podman/run/vm_run.go new file mode 100644 index 000000000..c01326381 --- /dev/null +++ b/scenarios/local/podman/run/vm_run.go @@ -0,0 +1,46 @@ +package localpodmanrun + +import ( + "github.com/DataDog/test-infra-definitions/components/datadog/agent" + "github.com/DataDog/test-infra-definitions/components/datadog/agentparams" + "github.com/DataDog/test-infra-definitions/resources/local" + localpodman "github.com/DataDog/test-infra-definitions/scenarios/local/podman" + + "github.com/DataDog/test-infra-definitions/components/datadog/fakeintake" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func VMRun(ctx *pulumi.Context) error { + env, err := local.NewEnvironment(ctx) + if err != nil { + return err + } + + vm, err := localpodman.NewVM(env, "podman-vm") + if err != nil { + return err + } + if err := vm.Export(ctx, nil); err != nil { + return err + } + + if env.AgentDeploy() { + agentOptions := []agentparams.Option{} + if env.AgentUseFakeintake() { + fakeintake, err := fakeintake.NewLocalDockerFakeintake(&env, "fakeintake") + if err != nil { + return err + } + err = fakeintake.Export(ctx, nil) + if err != nil { + return err + } + agentOptions = append(agentOptions, agentparams.WithFakeintake(fakeintake)) + } + agentOptions = append(agentOptions, agentparams.WithHostname("localpodman-vm")) + _, err = agent.NewHostAgent(&env, vm, agentOptions...) + return err + } + + return nil +} diff --git a/scenarios/local/podman/vm.go b/scenarios/local/podman/vm.go new file mode 100644 index 000000000..e833f9856 --- /dev/null +++ b/scenarios/local/podman/vm.go @@ -0,0 +1,39 @@ +package localpodman + +import ( + "github.com/DataDog/test-infra-definitions/components" + "github.com/DataDog/test-infra-definitions/components/command" + componentsos "github.com/DataDog/test-infra-definitions/components/os" + "github.com/DataDog/test-infra-definitions/components/remote" + "github.com/DataDog/test-infra-definitions/resources/local" + localpodman "github.com/DataDog/test-infra-definitions/resources/local/podman" + + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +// NewVM creates an Ubuntu container instance on podman that emulates a VM and returns a Host component. +func NewVM(e local.Environment, name string) (*remote.Host, error) { + // Create the EC2 instance + return components.NewComponent(&e, e.Namer.ResourceName(name), func(c *remote.Host) error { + vmArgs := &localpodman.VMArgs{ + Name: name, + } + + // Create the EC2 instance + address, user, port, err := localpodman.NewInstance(e, *vmArgs, pulumi.Parent(c)) + if err != nil { + return err + } + + // Create connection + conn, err := remote.NewConnection( + address, + user, + remote.WithPort(port), + ) + if err != nil { + return err + } + return remote.InitHost(&e, conn.ToConnectionOutput(), componentsos.Ubuntu2204, user, pulumi.String("").ToStringOutput(), command.WaitForSuccessfulConnection, c) + }) +} diff --git a/tasks/__init__.py b/tasks/__init__.py index a3f21970e..792199c19 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -5,7 +5,7 @@ import tasks.ci as ci import tasks.setup as setup import tasks.test as test -from tasks import aws, azure, gcp +from tasks import aws, azure, gcp, localpodman from tasks.aks import create_aks, destroy_aks from tasks.deploy import check_s3_image_exists from tasks.docker import create_docker, destroy_docker @@ -40,6 +40,7 @@ ns.add_collection(aws.collection, "aws") ns.add_collection(azure.collection, "az") ns.add_collection(gcp.collection, "gcp") +ns.add_collection(localpodman.collection, "localpodman") ns.add_collection(ci) ns.add_collection(setup) ns.add_collection(test) diff --git a/tasks/config.py b/tasks/config.py index 82f9d78ed..2e2b48c6d 100644 --- a/tasks/config.py +++ b/tasks/config.py @@ -52,6 +52,11 @@ class GCP(BaseModel, extra=Extra.forbid): gcp: Optional[GCP] = None + class Local(BaseModel, extra=Extra.forbid): + publicKeyPath: Optional[str] = None + + local: Optional[Local] = None + class Agent(BaseModel, extra=Extra.forbid): apiKey: Optional[str] appKey: Optional[str] @@ -106,6 +111,14 @@ def get_aws(self) -> Params.Aws: return default return self.configParams.aws + def get_local(self) -> Params.Local: + default = Config.Params.Local(publicKeyPath=None) + if self.configParams is None: + return default + if self.configParams.local is None: + return default + return self.configParams.local + def get_agent(self) -> Params.Agent: default = Config.Params.Agent(apiKey=None, appKey=None) if self.configParams is None: diff --git a/tasks/localpodman/__init__.py b/tasks/localpodman/__init__.py new file mode 100644 index 000000000..0d05afc19 --- /dev/null +++ b/tasks/localpodman/__init__.py @@ -0,0 +1,9 @@ +# type: ignore[reportArgumentType] + +from invoke.collection import Collection + +from tasks.localpodman.vm import create_vm, destroy_vm + +collection = Collection() +collection.add_task(destroy_vm) +collection.add_task(create_vm) diff --git a/tasks/localpodman/vm.py b/tasks/localpodman/vm.py new file mode 100644 index 000000000..0c7b9e5dc --- /dev/null +++ b/tasks/localpodman/vm.py @@ -0,0 +1,94 @@ +from typing import Optional + +from invoke.context import Context +from invoke.exceptions import Exit +from invoke.tasks import task +from pydantic_core._pydantic_core import ValidationError + +from tasks import config, doc +from tasks.deploy import deploy +from tasks.destroy import destroy +from tasks.tool import notify, show_connection_message + +scenario_name = "localpodman/vm" +remote_hostname = "local-podman-vm" + + +@task( + help={ + "config_path": doc.config_path, + "install_agent": doc.install_agent, + "pipeline_id": doc.pipeline_id, + "agent_version": doc.agent_version, + "stack_name": doc.stack_name, + "debug": doc.debug, + "use_fakeintake": doc.fakeintake, + "interactive": doc.interactive, + } +) +def create_vm( + ctx: Context, + config_path: Optional[str] = None, + stack_name: Optional[str] = None, + pipeline_id: Optional[str] = None, + install_agent: Optional[bool] = True, + agent_version: Optional[str] = None, + debug: Optional[bool] = False, + use_fakeintake: Optional[bool] = False, + interactive: Optional[bool] = True, +) -> None: + """ + Create a new virtual machine on local podman. + """ + + try: + cfg = config.get_local_config(config_path) + except ValidationError as e: + raise Exit(f"Error in config {config.get_full_profile_path(config_path)}") from e + + if not cfg.get_local().publicKeyPath: + raise Exit("The field `local.publicKeyPath` is required in the config file") + + extra_flags = { + "ddinfra:local/defaultPublicKeyPath": cfg.get_gcp().publicKeyPath, + } + + full_stack_name = deploy( + ctx, + scenario_name, + config_path, + stack_name=stack_name, + pipeline_id=pipeline_id, + install_agent=install_agent, + agent_version=agent_version, + debug=debug, + extra_flags=extra_flags, + use_fakeintake=use_fakeintake, + ) + + if interactive: + notify(ctx, "Your VM is now created") + + show_connection_message(ctx, remote_hostname, full_stack_name, interactive) + + +@task( + help={ + "config_path": doc.config_path, + "stack_name": doc.stack_name, + } +) +def destroy_vm( + ctx: Context, + config_path: Optional[str] = None, + stack_name: Optional[str] = None, +): + """ + Destroy a new virtual machine on aws. + """ + destroy( + ctx, + scenario_name=scenario_name, + config_path=config_path, + stack=stack_name, + ) diff --git a/tasks/tool.py b/tasks/tool.py index 388688131..961b49983 100644 --- a/tasks/tool.py +++ b/tasks/tool.py @@ -240,6 +240,7 @@ def __init__(self, name, stack_outputs: Any): remoteHost: Any = stack_outputs[f"dd-Host-{name}"] self.host: str = remoteHost["address"] self.user: str = remoteHost["username"] + self.port: int | None = "port" in remoteHost and remoteHost["port"] or None def show_connection_message( @@ -252,6 +253,9 @@ def show_connection_message( command = f"ssh {user}@{host}" + if remoteHost.port: + command += f" -p {remoteHost.port}" + print(f"\nYou can run the following command to connect to the host `{command}`.\n") if copy_to_clipboard: input("Press a key to copy command to clipboard...")