From 899f73aad344cc717e523f049fcf9141bde1fae1 Mon Sep 17 00:00:00 2001 From: Richard Palethorpe Date: Thu, 3 Oct 2024 17:17:23 +0100 Subject: [PATCH] Add AWS cloud start --- flake.nix | 2 +- go/cli/cloud/aws/lib.go | 787 ++++++++++++++++++++++++++++++++++++++++ go/cli/daemon/lib.go | 25 +- go/cmd/ay/main.go | 24 +- go/go.mod | 20 +- go/go.sum | 17 +- go/internal/conf/aws.go | 4 + 7 files changed, 860 insertions(+), 19 deletions(-) create mode 100644 go/cli/cloud/aws/lib.go 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..da6fd64 --- /dev/null +++ b/go/cli/cloud/aws/lib.go @@ -0,0 +1,787 @@ +package aws + +import ( + "context" + "errors" + "fmt" + "time" + + "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" + ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/aws-sdk-go-v2/service/iam" + iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + secretsmanagertypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" + + "github.com/aws/aws-sdk-go-v2/service/sts" + "go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws" + + "premai.io/Ayup/go/internal/terror" + "premai.io/Ayup/go/internal/trace" + "premai.io/Ayup/go/internal/tui" +) + +const ( + secretName = "ayup-preauth-conf" + iamRoleName = "ayup-read-preauth-secrets" + iamPolicyName = "ayup-read-preauth-secrets" + securityGroupName = "ayup-listen-tcp-50051" + subnetName = "ayup" + cidrBlock = "10.0.1.0/24" + vpcName = "ayup" + instanceProfileName = "ayup" + instanceType = "t2.micro" + amiID = "ami-0271107fdf337b99e" + instanceName = "ayup" +) + +func StartEc2(ctx context.Context, srvPeerId string, preauthConf string) error { + ctx, span := trace.Span(ctx, "start ec2") + defer span.End() + + cfg, err := config.LoadDefaultConfig(ctx, config.WithAppID("ayup-cli")) + if err != nil { + return terror.Errorf(ctx, "unable to load SDK config, %w", err) + } + otelaws.AppendMiddlewares(&cfg.APIOptions) + + smSvc := secretsmanager.NewFromConfig(cfg) + secretArn, err := createSecret(ctx, smSvc, preauthConf) + if err != nil { + return terror.Errorf(ctx, "Failed to create secret: %w", err) + } + + iamSvc := iam.NewFromConfig(cfg) + policyArn, err := createIAMPolicy(ctx, iamSvc, secretArn) + if err != nil { + return terror.Errorf(ctx, "Failed to create IAM policy: %w", err) + } + + roleArn, err := createIAMRole(ctx, iamSvc, policyArn) + if err != nil { + return terror.Errorf(ctx, "Failed to create IAM role: %w", err) + } + + ec2Svc := ec2.NewFromConfig(cfg) + vpcID, err := getOrCreateVPC(ctx, ec2Svc) + if err != nil { + return terror.Errorf(ctx, "Failed to get or create VPC: %w", err) + } + + securityGroupID, err := getOrCreateSecurityGroup(ctx, ec2Svc, vpcID) + if err != nil { + return terror.Errorf(ctx, "Failed to get or create security group: %w", err) + } + + subnetID, err := getOrCreateSubnet(ctx, ec2Svc, vpcID) + if err != nil { + return terror.Errorf(ctx, "Failed to get or create subnet: %w", err) + } + + instanceID, err := launchEC2Instance(ctx, ec2Svc, roleArn, securityGroupID, subnetID) + if err != nil { + return terror.Errorf(ctx, "Failed to launch EC2 instance: %w", err) + } + + instances, err := ec2Svc.DescribeInstances(ctx, &ec2.DescribeInstancesInput{ + InstanceIds: []string{instanceID}, + }) + if err != nil { + return terror.Errorf(ctx, "Failed to get EC2 instance: %w", err) + } + + ip4 := *instances.Reservations[0].Instances[0].PublicIpAddress + + fmt.Printf("Successfully launched EC2 instance with ID: %s\n", instanceID) + fmt.Printf("%s ay login /ip4/%s/tcp/50051/p2p/%s\n", + tui.TitleStyle.Render("Connect:"), ip4, srvPeerId) + + return nil +} + +func getAccountID(ctx context.Context, cfg aws.Config) (string, error) { + stsSvc := sts.NewFromConfig(cfg) + identity, err := stsSvc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) + if err != nil { + return "", err + } + return *identity.Account, nil +} + +func createIAMPolicy(ctx context.Context, svc *iam.Client, secretArn string) (string, error) { + policyDocument := fmt.Sprintf(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "secretsmanager:GetSecretValue", + "Resource": "%s" + } + ] + }`, secretArn) + + // Get current account ID + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return "", err + } + accountID, err := getAccountID(ctx, cfg) + if err != nil { + return "", err + } + + // Construct policy ARN + policyArn := fmt.Sprintf("arn:aws:iam::%s:policy/%s", accountID, iamPolicyName) + + _, err = svc.GetPolicy(ctx, &iam.GetPolicyInput{ + PolicyArn: aws.String(policyArn), + }) + if err != nil { + var notFoundErr *iamtypes.NoSuchEntityException + if errors.As(err, ¬FoundErr) { + // Policy does not exist, create a new one + createPolicyOutput, err := svc.CreatePolicy(ctx, &iam.CreatePolicyInput{ + PolicyName: aws.String(iamPolicyName), + PolicyDocument: aws.String(policyDocument), + }) + if err != nil { + return "", err + } + return *createPolicyOutput.Policy.Arn, nil + } + return "", err + } + + // Policy exists, create a new policy version + _, err = svc.CreatePolicyVersion(ctx, &iam.CreatePolicyVersionInput{ + PolicyArn: aws.String(policyArn), + PolicyDocument: aws.String(policyDocument), + SetAsDefault: true, + }) + if err != nil { + return "", err + } + + // List policy versions + listPolicyVersionsOutput, err := svc.ListPolicyVersions(ctx, &iam.ListPolicyVersionsInput{ + PolicyArn: aws.String(policyArn), + }) + if err != nil { + return "", err + } + + // Delete the oldest policy version if there are more than 4 versions + if len(listPolicyVersionsOutput.Versions) > 4 { + var oldestVersion *iamtypes.PolicyVersion + for _, version := range listPolicyVersionsOutput.Versions { + if !version.IsDefaultVersion { + if oldestVersion == nil || version.CreateDate.Before(*oldestVersion.CreateDate) { + oldestVersion = &version + } + } + } + + _, err = svc.DeletePolicyVersion(ctx, &iam.DeletePolicyVersionInput{ + PolicyArn: aws.String(policyArn), + VersionId: oldestVersion.VersionId, + }) + if err != nil { + return "", err + } + } + + return policyArn, 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" + } + ] + }` + + // Check if the role exists + _, err := svc.GetRole(ctx, &iam.GetRoleInput{ + RoleName: aws.String(iamRoleName), + }) + if err != nil { + var notFoundErr *iamtypes.NoSuchEntityException + if errors.As(err, ¬FoundErr) { + // Role does not exist, create a new one + _, err := svc.CreateRole(ctx, &iam.CreateRoleInput{ + RoleName: aws.String(iamRoleName), + AssumeRolePolicyDocument: aws.String(assumeRolePolicyDocument), + }) + if err != nil { + return "", err + } + } else { + return "", err + } + } + + // Check if the policy is attached to the role + attachedPolicies, err := svc.ListAttachedRolePolicies(ctx, &iam.ListAttachedRolePoliciesInput{ + RoleName: aws.String(iamRoleName), + }) + if err != nil { + return "", err + } + + policyAttached := false + for _, attachedPolicy := range attachedPolicies.AttachedPolicies { + if *attachedPolicy.PolicyArn == policyArn { + policyAttached = true + break + } + } + + if !policyAttached { + // Attach the policy to the role + _, err = svc.AttachRolePolicy(ctx, &iam.AttachRolePolicyInput{ + RoleName: aws.String(iamRoleName), + PolicyArn: aws.String(policyArn), + }) + if err != nil { + return "", err + } + } + + // Check if the instance profile exists + _, err = svc.GetInstanceProfile(ctx, &iam.GetInstanceProfileInput{ + InstanceProfileName: aws.String(instanceProfileName), + }) + if err != nil { + var notFoundErr *iamtypes.NoSuchEntityException + if errors.As(err, ¬FoundErr) { + // Instance profile does not exist, create a new one + _, err = svc.CreateInstanceProfile(ctx, &iam.CreateInstanceProfileInput{ + InstanceProfileName: aws.String(instanceProfileName), + }) + if err != nil { + return "", err + } + } else { + return "", err + } + } + + // Check if the role is added to the instance profile + instanceProfile, err := svc.GetInstanceProfile(ctx, &iam.GetInstanceProfileInput{ + InstanceProfileName: aws.String(instanceProfileName), + }) + if err != nil { + return "", err + } + + roleExistsInProfile := false + for _, role := range instanceProfile.InstanceProfile.Roles { + if *role.RoleName == iamRoleName { + roleExistsInProfile = true + break + } + } + + if !roleExistsInProfile { + // Add the role to the instance profile + _, err = svc.AddRoleToInstanceProfile(ctx, &iam.AddRoleToInstanceProfileInput{ + InstanceProfileName: aws.String(instanceProfileName), + RoleName: aws.String(iamRoleName), + }) + if err != nil { + return "", err + } + } + + return *instanceProfile.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"), + TagSpecifications: []ec2types.TagSpecification{ + { + ResourceType: ec2types.ResourceTypeVpc, + 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("Allow inbound traffic on 50051 for the Ayup CLI"), + 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 getOrCreateSubnet(ctx context.Context, svc *ec2.Client, vpcID, cidrBlock, subnetName string) (string, error) { + // Check if the subnet already exists within the VPC + describeSubnetsOutput, err := svc.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{ + Filters: []ec2types.Filter{ + { + Name: aws.String("vpc-id"), + Values: []string{vpcID}, + }, + { + Name: aws.String("cidr-block"), + Values: []string{cidrBlock}, + }, + }, + }) + if err != nil { + return "", err + } + + if len(describeSubnetsOutput.Subnets) > 0 { + // Subnet exists, return the subnet ID + subnetID := *describeSubnetsOutput.Subnets[0].SubnetId + fmt.Printf("Subnet %s already exists in VPC %s.\n", subnetID, vpcID) + return subnetID, nil + } + + // Subnet does not exist, create a new one + createSubnetOutput, err := svc.CreateSubnet(ctx, &ec2.CreateSubnetInput{ + VpcId: aws.String(vpcID), + CidrBlock: aws.String(cidrBlock), + }) + if err != nil { + return "", err + } + + subnetID := *createSubnetOutput.Subnet.SubnetId + + // Add Name tag to the subnet + _, err = svc.CreateTags(ctx, &ec2.CreateTagsInput{ + Resources: []string{subnetID}, + Tags: []ec2types.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(subnetName), + }, + }, + }) + if err != nil { + return "", err + } + + // Modify the Subnet attribute to enable auto-assign public IP addresses + _, err = svc.ModifySubnetAttribute(ctx, &ec2.ModifySubnetAttributeInput{ + SubnetId: aws.String(subnetID), + MapPublicIpOnLaunch: &ec2types.AttributeBooleanValue{ + Value: aws.Bool(true), + }, + }) + if err != nil { + return "", err + } + fmt.Printf("Enabled auto-assign public IP on Subnet %s\n", subnetID) + + // Check if an Internet Gateway already exists for the VPC + describeIGWsOutput, err := svc.DescribeInternetGateways(ctx, &ec2.DescribeInternetGatewaysInput{ + Filters: []ec2types.Filter{ + { + Name: aws.String("attachment.vpc-id"), + Values: []string{vpcID}, + }, + }, + }) + if err != nil { + return "", err + } + + var igwID string + if len(describeIGWsOutput.InternetGateways) > 0 { + // Internet Gateway exists, get its ID + igwID = *describeIGWsOutput.InternetGateways[0].InternetGatewayId + fmt.Printf("Internet Gateway %s already exists for VPC %s.\n", igwID, vpcID) + } else { + // Create a new Internet Gateway + createIGWOutput, err := svc.CreateInternetGateway(ctx, &ec2.CreateInternetGatewayInput{}) + if err != nil { + return "", err + } + igwID = *createIGWOutput.InternetGateway.InternetGatewayId + + // Attach the Internet Gateway to the VPC + _, err = svc.AttachInternetGateway(ctx, &ec2.AttachInternetGatewayInput{ + InternetGatewayId: aws.String(igwID), + VpcId: aws.String(vpcID), + }) + if err != nil { + return "", err + } + fmt.Printf("Created and attached Internet Gateway %s to VPC %s.\n", igwID, vpcID) + } + + // Create a Route Table + createRTOutput, err := svc.CreateRouteTable(ctx, &ec2.CreateRouteTableInput{ + VpcId: aws.String(vpcID), + }) + if err != nil { + return "", err + } + rtID := *createRTOutput.RouteTable.RouteTableId + + // Create a route to the Internet Gateway in the Route Table + _, err = svc.CreateRoute(ctx, &ec2.CreateRouteInput{ + RouteTableId: aws.String(rtID), + DestinationCidrBlock: aws.String("0.0.0.0/0"), + GatewayId: aws.String(igwID), + }) + if err != nil { + return "", err + } + fmt.Printf("Created route to Internet Gateway %s in Route Table %s.\n", igwID, rtID) + + // Associate the Route Table with the Subnet + _, err = svc.AssociateRouteTable(ctx, &ec2.AssociateRouteTableInput{ + RouteTableId: aws.String(rtID), + SubnetId: aws.String(subnetID), + }) + if err != nil { + return "", err + } + fmt.Printf("Associated Route Table %s with Subnet %s.\n", rtID, subnetID) + + return subnetID, nil +} + +func createSecret(ctx context.Context, svc *secretsmanager.Client, secretValue string) (string, error) { + // Check if the secret already exists + _, err := svc.DescribeSecret(ctx, &secretsmanager.DescribeSecretInput{ + SecretId: aws.String(secretName), + }) + + if err == nil { + // Secret exists, update it + output, err := svc.UpdateSecret(ctx, &secretsmanager.UpdateSecretInput{ + SecretId: aws.String(secretName), + SecretString: aws.String(secretValue), + }) + if err != nil { + return "", err + } + return *output.ARN, nil + } + + var notFoundErr *secretsmanagertypes.ResourceNotFoundException + if errors.As(err, ¬FoundErr) { + // Secret does not exist, create a new one + output, err := svc.CreateSecret(ctx, &secretsmanager.CreateSecretInput{ + Name: aws.String(secretName), + SecretString: aws.String(secretValue), + }) + if err != nil { + return "", err + } + return *output.ARN, nil + } + + return "", err +} + +func launchEC2Instance(ctx context.Context, svc *ec2.Client, roleArn, securityGroupID, subnetId string) (string, error) { + // Check if an instance with the same name tag is already running + describeInstancesOutput, err := svc.DescribeInstances(ctx, &ec2.DescribeInstancesInput{ + Filters: []ec2types.Filter{ + { + Name: aws.String("tag:Name"), + Values: []string{instanceName}, + }, + { + Name: aws.String("instance-state-name"), + Values: []string{"running", "pending", "stopping", "stopped"}, + }, + }, + }) + if err != nil { + return "", err + } + + for _, reservation := range describeInstancesOutput.Reservations { + for _, instance := range reservation.Instances { + instanceID := *instance.InstanceId + instanceState := instance.State.Name + + switch instanceState { + case "running": + fmt.Printf("Instance %s is already running. Restarting it...\n", instanceID) + // Stop the instance + _, err := svc.StopInstances(ctx, &ec2.StopInstancesInput{ + InstanceIds: []string{instanceID}, + }) + if err != nil { + return "", err + } + + // Wait until the instance is stopped + stoppedWaiter := ec2.NewInstanceStoppedWaiter(svc) + err = stoppedWaiter.Wait(ctx, &ec2.DescribeInstancesInput{ + InstanceIds: []string{instanceID}, + }, 5*time.Minute) + if err != nil { + return "", err + } + + // Start the instance + _, err = svc.StartInstances(ctx, &ec2.StartInstancesInput{ + InstanceIds: []string{instanceID}, + }) + if err != nil { + return "", err + } + + // Wait until the instance is running + runningWaiter := ec2.NewInstanceRunningWaiter(svc) + err = runningWaiter.Wait(ctx, &ec2.DescribeInstancesInput{ + InstanceIds: []string{instanceID}, + }, 5*time.Minute) + if err != nil { + return "", err + } + + // Wait until the instance status is OK + err = waitForInstanceStatusOK(ctx, svc, instanceID) + if err != nil { + return "", err + } + + return instanceID, nil + case "stopped": + fmt.Printf("Instance %s is stopped. Starting it...\n", instanceID) + _, err := svc.StartInstances(ctx, &ec2.StartInstancesInput{ + InstanceIds: []string{instanceID}, + }) + if err != nil { + return "", err + } + // Wait until the instance is running + runningWaiter := ec2.NewInstanceRunningWaiter(svc) + err = runningWaiter.Wait(ctx, &ec2.DescribeInstancesInput{ + InstanceIds: []string{instanceID}, + }, 5*time.Minute) + if err != nil { + return "", err + } + // Wait until the instance status is OK + err = waitForInstanceStatusOK(ctx, svc, instanceID) + if err != nil { + return "", err + } + return instanceID, nil + case "stopping": + fmt.Printf("Instance %s is stopping. Waiting for it to stop...\n", instanceID) + stoppedWaiter := ec2.NewInstanceStoppedWaiter(svc) + err := stoppedWaiter.Wait(ctx, &ec2.DescribeInstancesInput{ + InstanceIds: []string{instanceID}, + }, 5*time.Minute) + if err != nil { + return "", err + } + fmt.Printf("Instance %s is now stopped. Starting it...\n", instanceID) + _, err = svc.StartInstances(ctx, &ec2.StartInstancesInput{ + InstanceIds: []string{instanceID}, + }) + if err != nil { + return "", err + } + // Wait until the instance is running + runningWaiter := ec2.NewInstanceRunningWaiter(svc) + err = runningWaiter.Wait(ctx, &ec2.DescribeInstancesInput{ + InstanceIds: []string{instanceID}, + }, 5*time.Minute) + if err != nil { + return "", err + } + // Wait until the instance status is OK + err = waitForInstanceStatusOK(ctx, svc, instanceID) + if err != nil { + return "", err + } + return instanceID, nil + case "pending": + fmt.Printf("Instance %s is pending. Waiting for it to run...\n", instanceID) + runningWaiter := ec2.NewInstanceRunningWaiter(svc) + err := runningWaiter.Wait(ctx, &ec2.DescribeInstancesInput{ + InstanceIds: []string{instanceID}, + }, 5*time.Minute) + if err != nil { + return "", err + } + // Wait until the instance status is OK + err = waitForInstanceStatusOK(ctx, svc, instanceID) + if err != nil { + return "", err + } + return instanceID, nil + } + } + } + + // No existing instance found, launch a new one + runResult, err := svc.RunInstances(ctx, &ec2.RunInstancesInput{ + ImageId: aws.String(amiID), + InstanceType: ec2types.InstanceTypeT2Micro, + MinCount: aws.Int32(1), + MaxCount: aws.Int32(1), + IamInstanceProfile: &ec2types.IamInstanceProfileSpecification{ + Arn: aws.String(roleArn), + }, + SecurityGroupIds: []string{securityGroupID}, + SubnetId: &subnetId, + TagSpecifications: []ec2types.TagSpecification{ + { + ResourceType: ec2types.ResourceTypeInstance, + Tags: []ec2types.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(instanceName), + }, + }, + }, + }, + }) + if err != nil { + return "", err + } + + instanceID := *runResult.Instances[0].InstanceId + + // Wait until the instance is running + runningWaiter := ec2.NewInstanceRunningWaiter(svc) + err = runningWaiter.Wait(ctx, &ec2.DescribeInstancesInput{ + InstanceIds: []string{instanceID}, + }, 5*time.Minute) + if err != nil { + return "", err + } + + // Wait until the instance status is OK + err = waitForInstanceStatusOK(ctx, svc, instanceID) + if err != nil { + return "", err + } + + return instanceID, nil +} + +func waitForInstanceStatusOK(ctx context.Context, svc *ec2.Client, instanceID string) error { + // Create a context with a 5-minute timeout + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) + defer cancel() + + var lastStatus *ec2types.InstanceStatus + + for { + select { + case <-timeoutCtx.Done(): + fmt.Printf("Timed out waiting for instance %s status to be OK. Last status: %+v\n", instanceID, lastStatus) + return fmt.Errorf("timed out waiting for instance %s status to be OK", instanceID) + default: + descResult, err := svc.DescribeInstanceStatus(ctx, &ec2.DescribeInstanceStatusInput{ + InstanceIds: []string{instanceID}, + }) + if err != nil { + return err + } + + if len(descResult.InstanceStatuses) > 0 { + status := descResult.InstanceStatuses[0] + lastStatus = &status + if status.InstanceStatus.Status == ec2types.SummaryStatusOk && status.SystemStatus.Status == ec2types.SummaryStatusOk { + fmt.Printf("Instance %s status is OK.\n", instanceID) + return nil + } + } + + fmt.Printf("Waiting for instance %s status to be OK...\n", instanceID) + time.Sleep(10 * time.Second) + } + } +} diff --git a/go/cli/daemon/lib.go b/go/cli/daemon/lib.go index 470e36e..69475c0 100644 --- a/go/cli/daemon/lib.go +++ b/go/cli/daemon/lib.go @@ -16,35 +16,35 @@ import ( "premai.io/Ayup/go/internal/tui" ) -func RunPreauth(ctx context.Context, b64privKey string) error { +func PreauthConf(ctx context.Context, cliPrivKeyB64 string) (peer.ID, string, error) { ctx, span := trace.Span(ctx, "preauth") defer span.End() - cliPrivKey, err := rpc.EnsurePrivKey(ctx, "AYUP_CLIENT_P2P_PRIV_KEY", b64privKey) + cliPrivKey, err := rpc.EnsurePrivKey(ctx, "AYUP_CLIENT_P2P_PRIV_KEY", cliPrivKeyB64) if err != nil { - return err + return peer.ID(""), "", err } cliPeerId, err := peer.IDFromPrivateKey(cliPrivKey) if err != nil { - return terror.Errorf(ctx, "peer IDFromPrivateKey: %w", err) + return peer.ID(""), "", terror.Errorf(ctx, "peer IDFromPrivateKey: %w", err) } srvPrivKey, pub, err := crypto.GenerateEd25519Key(nil) if err != nil { - return terror.Errorf(ctx, "crypto GenerateEd25519Key: %w", err) + return peer.ID(""), "", terror.Errorf(ctx, "crypto GenerateEd25519Key: %w", err) } pbSrvPrivKey, err := crypto.MarshalPrivateKey(srvPrivKey) if err != nil { - return terror.Errorf(ctx, "crypto marshalPrivateKey: %w", err) + return peer.ID(""), "", terror.Errorf(ctx, "crypto marshalPrivateKey: %w", err) } b64SrvPrivKey := base64.StdEncoding.EncodeToString(pbSrvPrivKey) srvPeerId, err := peer.IDFromPublicKey(pub) if err != nil { - return terror.Errorf(ctx, "peer IDFromPublicKey: %w", err) + return peer.ID(""), "", terror.Errorf(ctx, "peer IDFromPublicKey: %w", err) } confMap := map[string]string{ @@ -54,7 +54,16 @@ func RunPreauth(ctx context.Context, b64privKey string) error { confText, err := godotenv.Marshal(confMap) if err != nil { - return terror.Errorf(ctx, "godotenv Marshal: %w", err) + return peer.ID(""), "", terror.Errorf(ctx, "godotenv Marshal: %w", err) + } + + return srvPeerId, confText, nil +} + +func RunPreauth(ctx context.Context, b64privKey string) error { + srvPeerId, confText, err := PreauthConf(ctx, b64privKey) + if err != nil { + return err } fmt.Fprintln(os.Stderr, tui.VersionStyle.Render("Printing server environment variables to stdout. You can save this configuration to ~/.config/ayup/env or set the environment some other way")) diff --git a/go/cmd/ay/main.go b/go/cmd/ay/main.go index 1c4327c..35c7cb4 100644 --- a/go/cmd/ay/main.go +++ b/go/cmd/ay/main.go @@ -20,6 +20,7 @@ import ( "github.com/muesli/termenv" "premai.io/Ayup/go/cli/assistants" + "premai.io/Ayup/go/cli/cloud/aws" "premai.io/Ayup/go/cli/daemon" "premai.io/Ayup/go/cli/key" "premai.io/Ayup/go/cli/login" @@ -37,12 +38,25 @@ 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"` +} + +func (s *AwsStartCmd) Run(g Globals) (err error) { + srvPeerId, preauthConf, err := daemon.PreauthConf(g.Ctx, s.CliP2pPrivKey) + if err != nil { + return err + } + + return aws.StartEc2(g.Ctx, srvPeerId.String(), preauthConf) +} + 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 +158,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..d1a2f11 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 @@ -39,14 +43,19 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) +require ( + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.18 // indirect + github.com/aws/aws-sdk-go-v2/service/sqs v1.34.8 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect +) + require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect 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,10 +63,9 @@ 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 + github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 github.com/aws/smithy-go v1.21.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/benbjohnson/clock v1.3.5 // indirect @@ -113,7 +121,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 @@ -195,6 +202,7 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect github.com/wlynxg/anet v0.0.3 // indirect go.opentelemetry.io/contrib v1.17.0 // indirect + go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.55.0 go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect diff --git a/go/go.sum b/go/go.sum index fa7ddd9..17332e2 100644 --- a/go/go.sum +++ b/go/go.sum @@ -47,12 +47,22 @@ 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/dynamodb v1.34.9 h1:jbqgtdKfAXebx2/l2UhDEe/jmmCIhaCO3HFK71M7VzM= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.9/go.mod h1:N3YdUYxyxhiuAelUgCpSVBuBI1klobJxZrDtL+olu10= +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/endpoint-discovery v1.9.18 h1:GACdEPdpBE59I7pbfvu0/Mw1wzstlP3QtPHklUxybFE= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.18/go.mod h1:K+xV06+Wni4TSaOOJ1Y35e5tYOCUBYbebLKmJQQa8yY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.3 h1:W2M3kQSuN1+FXgV2wMv1JMWPxw/37wBN87QHYDuTV0Y= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.3/go.mod h1:WyLS5qwXHtjKAONYZq/4ewdd+hcVsa3LBu77Ow5uj3k= +github.com/aws/aws-sdk-go-v2/service/sqs v1.34.8 h1:t3TzmBX0lpDNtLhl7vY97VMvLtxp/KTvjjj2X3s6SUQ= +github.com/aws/aws-sdk-go-v2/service/sqs v1.34.8/go.mod h1:zn0Oy7oNni7XIGoAd6bHBTVtX06OrnpvT1kww8jxyi8= github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 h1:rs4JCczF805+FDv2tRhZ1NU0RB2H6ryAvsWPanAr72Y= github.com/aws/aws-sdk-go-v2/service/sso v1.23.3/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 h1:S7EPdMVZod8BGKQQPTBK+FcX9g7bKR7c4+HxWqHP7Vg= @@ -259,6 +269,7 @@ github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPw 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 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 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= @@ -569,8 +580,8 @@ 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/github.com/aws/aws-sdk-go-v2/otelaws v0.55.0 h1:MnAevUB0SFfKALzF5ApgrArdvHZduRT3/e59L/lNYKE= +go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.55.0/go.mod h1:MHPbT1EvQOZMGbKeuCovYWcyM9iaxcltRf7+GsU8ziE= 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= @@ -803,6 +814,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go/internal/conf/aws.go b/go/internal/conf/aws.go index 3a16e47..ef6427d 100644 --- a/go/internal/conf/aws.go +++ b/go/internal/conf/aws.go @@ -6,6 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws" + "premai.io/Ayup/go/internal/terror" ) @@ -15,6 +17,8 @@ func LoadConfigFromAWS(ctx context.Context) (string, error) { return "", terror.Errorf(ctx, "config LoadDefaultConfig: %w", err) } + otelaws.AppendMiddlewares(&cfg.APIOptions) + svc := secretsmanager.NewFromConfig(cfg) secretValue, err := getSecretValue(ctx, svc, "ayup-preauth-conf")