diff --git a/flake.nix b/flake.nix index 074d1d2..8115eb3 100644 --- a/flake.nix +++ b/flake.nix @@ -45,7 +45,7 @@ inherit version; dontPatchShebangs = true; }; - vendorHash = "sha256-OmNKmih4OSJSp5Thuotn6SB/TLDvMHNmwFdzJxXyAe4="; #pkgs.lib.fakeHash; + vendorHash = pkgs.lib.fakeHash; cli = pkgs.callPackage ./distros/nix/cli.nix { inherit version vendorHash; }; diff --git a/go/cli/cloud/aws/lib.go b/go/cli/cloud/aws/lib.go new file mode 100644 index 0000000..8ae4531 --- /dev/null +++ b/go/cli/cloud/aws/lib.go @@ -0,0 +1,282 @@ +package aws + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + + "premai.io/Ayup/go/internal/terror" +) + +const ( + secretName = "ayup-preauth-conf" + iamRoleName = "ayup-read-preauth-secrets" + iamPolicyName = "ayup-read-preauth-secrets" + securityGroupName = "ayup-listen-tcp-50051" + vpcName = "ayup" + instanceProfileName = "ayup" + instanceType = "t2.micro" + keyName = "ayup-debug" + amiID = "ami-0abcdef1234567890" // Replace with a valid AMI ID +) + +func StartEc2(ctx context.Context) error { + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return terror.Errorf(ctx, "unable to load SDK config, %v", err) + } + + // Create IAM policy + iamSvc := iam.NewFromConfig(cfg) + policyArn, err := createIAMPolicy(ctx, iamSvc) + if err != nil { + return terror.Errorf(ctx, "Failed to create IAM policy: %v", err) + } + + // Create IAM role and attach policy + roleArn, err := createIAMRole(ctx, iamSvc, policyArn) + if err != nil { + return terror.Errorf(ctx, "Failed to create IAM role: %v", err) + } + + // Create or get VPC + ec2Svc := ec2.NewFromConfig(cfg) + vpcID, err := getOrCreateVPC(ctx, ec2Svc) + if err != nil { + return terror.Errorf(ctx, "Failed to get or create VPC: %v", err) + } + + // Create or get security group + securityGroupID, err := getOrCreateSecurityGroup(ctx, ec2Svc, vpcID) + if err != nil { + return terror.Errorf(ctx, "Failed to get or create security group: %v", err) + } + + // Create secret in Secrets Manager + smSvc := secretsmanager.NewFromConfig(cfg) + err = createSecret(ctx, smSvc) + if err != nil { + return terror.Errorf(ctx, "Failed to create secret: %v", err) + } + + // Launch EC2 instance + instanceID, err := launchEC2Instance(ctx, ec2Svc, roleArn, securityGroupID) + if err != nil { + return terror.Errorf(ctx, "Failed to launch EC2 instance: %v", err) + } + + fmt.Printf("Successfully launched EC2 instance with ID: %s\n", instanceID) + + return nil +} + +func createIAMPolicy(ctx context.Context, svc *iam.Client) (string, error) { + policyDocument := `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "secretsmanager:GetSecretValue", + "Resource": "*" + } + ] + }` + + result, err := svc.CreatePolicy(ctx, &iam.CreatePolicyInput{ + PolicyName: aws.String(iamPolicyName), + PolicyDocument: aws.String(policyDocument), + }) + if err != nil { + return "", err + } + + return *result.Policy.Arn, nil +} + +func createIAMRole(ctx context.Context, svc *iam.Client, policyArn string) (string, error) { + assumeRolePolicyDocument := `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + }` + + _, err := svc.CreateRole(ctx, &iam.CreateRoleInput{ + RoleName: aws.String(iamRoleName), + AssumeRolePolicyDocument: aws.String(assumeRolePolicyDocument), + }) + if err != nil { + return "", err + } + + _, err = svc.AttachRolePolicy(ctx, &iam.AttachRolePolicyInput{ + RoleName: aws.String(iamRoleName), + PolicyArn: aws.String(policyArn), + }) + if err != nil { + return "", err + } + + profileResult, err := svc.CreateInstanceProfile(ctx, &iam.CreateInstanceProfileInput{ + InstanceProfileName: aws.String(instanceProfileName), + }) + if err != nil { + return "", err + } + + _, err = svc.AddRoleToInstanceProfile(ctx, &iam.AddRoleToInstanceProfileInput{ + InstanceProfileName: aws.String(instanceProfileName), + RoleName: aws.String(iamRoleName), + }) + if err != nil { + return "", err + } + + return *profileResult.InstanceProfile.Arn, nil +} + +func getOrCreateVPC(ctx context.Context, svc *ec2.Client) (string, error) { + // Check if VPC exists + result, err := svc.DescribeVpcs(ctx, &ec2.DescribeVpcsInput{ + Filters: []ec2types.Filter{ + { + Name: aws.String("tag:Name"), + Values: []string{vpcName}, + }, + }, + }) + if err != nil { + return "", err + } + + if len(result.Vpcs) > 0 { + return *result.Vpcs[0].VpcId, nil + } + + // Create VPC if it doesn't exist + createResult, err := svc.CreateVpc(ctx, &ec2.CreateVpcInput{ + CidrBlock: aws.String("10.0.0.0/16"), + }) + if err != nil { + return "", err + } + + // Add Name tag to VPC + _, err = svc.CreateTags(ctx, &ec2.CreateTagsInput{ + Resources: []string{*createResult.Vpc.VpcId}, + Tags: []ec2types.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(vpcName), + }, + }, + }) + if err != nil { + return "", err + } + + return *createResult.Vpc.VpcId, nil +} + +func getOrCreateSecurityGroup(ctx context.Context, svc *ec2.Client, vpcID string) (string, error) { + // Check if security group exists + result, err := svc.DescribeSecurityGroups(ctx, &ec2.DescribeSecurityGroupsInput{ + Filters: []ec2types.Filter{ + { + Name: aws.String("group-name"), + Values: []string{securityGroupName}, + }, + { + Name: aws.String("vpc-id"), + Values: []string{vpcID}, + }, + }, + }) + if err != nil { + return "", err + } + + if len(result.SecurityGroups) > 0 { + return *result.SecurityGroups[0].GroupId, nil + } + + // Create security group if it doesn't exist + createResult, err := svc.CreateSecurityGroup(ctx, &ec2.CreateSecurityGroupInput{ + GroupName: aws.String(securityGroupName), + Description: aws.String("Security group for EC2 instance"), + VpcId: aws.String(vpcID), + }) + if err != nil { + return "", err + } + + // Authorize inbound traffic on port 50051/tcp + _, err = svc.AuthorizeSecurityGroupIngress(ctx, &ec2.AuthorizeSecurityGroupIngressInput{ + GroupId: aws.String(*createResult.GroupId), + IpPermissions: []ec2types.IpPermission{ + { + IpProtocol: aws.String("tcp"), + FromPort: aws.Int32(50051), + ToPort: aws.Int32(50051), + IpRanges: []ec2types.IpRange{ + { + CidrIp: aws.String("0.0.0.0/0"), + }, + }, + }, + }, + }) + if err != nil { + return "", err + } + + return *createResult.GroupId, nil +} + +func createSecret(ctx context.Context, svc *secretsmanager.Client) error { + _, err := svc.CreateSecret(ctx, &secretsmanager.CreateSecretInput{ + Name: aws.String(secretName), + SecretString: aws.String(secretValue), + }) + return err +} + +func launchEC2Instance(ctx context.Context, svc *ec2.Client, roleArn, securityGroupID string) (string, error) { + runResult, err := svc.RunInstances(ctx, &ec2.RunInstancesInput{ + ImageId: aws.String(amiID), + InstanceType: ec2types.InstanceTypeT2Micro, + KeyName: aws.String(keyName), + MinCount: aws.Int32(1), + MaxCount: aws.Int32(1), + IamInstanceProfile: &ec2types.IamInstanceProfileSpecification{ + Arn: aws.String(roleArn), + }, + SecurityGroupIds: []string{securityGroupID}, + }) + if err != nil { + return "", err + } + + instanceID := *runResult.Instances[0].InstanceId + + // Wait for the instance to be in running state + err = svc.WaitUntilInstanceRunning(ctx, &ec2.DescribeInstancesInput{ + InstanceIds: []string{instanceID}, + }) + if err != nil { + return "", err + } + + return instanceID, nil +} diff --git a/go/cmd/ay/main.go b/go/cmd/ay/main.go index 1c4327c..630a467 100644 --- a/go/cmd/ay/main.go +++ b/go/cmd/ay/main.go @@ -37,12 +37,16 @@ type Globals struct { Tracer trace.Tracer } +type AwsStartCmd struct { + CliP2pPrivKey string `env:"AYUP_CLIENT_P2P_PRIV_KEY" help:"The client's private key, generated automatically if not set"` +} + type DaemonPreauthCmd struct { - P2pPrivKey string `env:"AYUP_CLIENT_P2P_PRIV_KEY" help:"The client's private key, generated automatically if not set"` + CliP2pPrivKey string `env:"AYUP_CLIENT_P2P_PRIV_KEY" help:"The client's private key, generated automatically if not set"` } func (s *DaemonPreauthCmd) Run(g Globals) (err error) { - return daemon.RunPreauth(g.Ctx, s.P2pPrivKey) + return daemon.RunPreauth(g.Ctx, s.CliP2pPrivKey) } type PushCmd struct { @@ -144,6 +148,12 @@ func (s *AssistantsList) Run(g Globals) error { var cli struct { Login LoginCmd `group:"Client:" cmd:"" help:"Login to the Ayup service"` + Cloud struct { + Aws struct { + Start AwsStartCmd `cmd:"" help:"Start, and authenticate with, an Ayup server on your AWS account"` + } `cmd:"" help:"Amazon"` + } `group:"Server:" cmd:"" help:"Host Ayup using cloud providers"` + Daemon struct { Start DaemonStartCmd `cmd:"" help:"Start an Ayup service Daemon"` StartInRootless DaemonStartInRootlessCmd `cmd:"" passthrough:"" help:"Start a utility daemon to do tasks such as port forwarding in the Rootlesskit namesapce" hidden:""` diff --git a/go/go.mod b/go/go.mod index 951a377..187d16a 100644 --- a/go/go.mod +++ b/go/go.mod @@ -4,6 +4,11 @@ go 1.22.7 require ( github.com/alecthomas/kong v1.2.1 + github.com/aws/aws-sdk-go-v2 v1.31.0 + github.com/aws/aws-sdk-go-v2/config v1.27.39 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.179.2 + github.com/aws/aws-sdk-go-v2/service/iam v1.36.3 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.3 github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbletea v1.1.1 github.com/charmbracelet/huh v0.6.0 @@ -22,7 +27,6 @@ require ( github.com/multiformats/go-multiaddr v0.13.0 github.com/opencontainers/go-digest v1.0.0 github.com/tonistiigi/fsutil v0.0.0-20240902111258-43b9329361d9 - go.opentelemetry.io/contrib/bridges/otelslog v0.5.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 go.opentelemetry.io/otel v1.30.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.6.0 @@ -45,8 +49,6 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2 v1.31.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.27.39 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.37 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect @@ -54,7 +56,6 @@ require ( github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect - github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.3 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 // indirect @@ -113,7 +114,6 @@ require ( github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/koron/go-ssdp v0.0.4 // indirect diff --git a/go/go.sum b/go/go.sum index fa7ddd9..836ea24 100644 --- a/go/go.sum +++ b/go/go.sum @@ -47,6 +47,10 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7Yuht github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.179.2 h1:rGBv2N0zWvNTKnxOfbBH4mNM8WMdDNkaxdqtz152G40= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.179.2/go.mod h1:W6sNzs5T4VpZn1Vy+FMKw8s24vt5k6zPJXcNOK0asBo= +github.com/aws/aws-sdk-go-v2/service/iam v1.36.3 h1:dV9iimLEHKYAz2qTi+tGAD9QCnAG2pLD7HUEHB7m4mI= +github.com/aws/aws-sdk-go-v2/service/iam v1.36.3/go.mod h1:HSvujsK8xeEHMIB18oMXjSfqaN9cVqpo/MtHJIksQRk= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 h1:QFASJGfT8wMXtuP3D5CRmMjARHv9ZmzFUMJznHDOY3w= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5/go.mod h1:QdZ3OmoIjSX+8D1OPAzPxDfjXASbBMDsz9qvtyIhtik= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg= @@ -257,9 +261,6 @@ github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+ github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -569,8 +570,6 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib v1.17.0 h1:lJJdtuNsP++XHD7tXDYEFSpsqIc7DzShuXMR5PwkmzA= go.opentelemetry.io/contrib v1.17.0/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs= -go.opentelemetry.io/contrib/bridges/otelslog v0.5.0 h1:lU3F57OSLK5mQ1PDBVAfDDaKCPv37MrEbCfTzsF4bz0= -go.opentelemetry.io/contrib/bridges/otelslog v0.5.0/go.mod h1:I84u06zJFr8T5D73fslEUbnRBimVVSBhuVw8L8I92AU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=