github.com/navikt/knorten@v0.0.0-20240419132333-1333f46ed8b6/pkg/user/gcp.go (about)

     1  package user
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os/exec"
     8  	"strings"
     9  
    10  	"github.com/navikt/knorten/pkg/gcp"
    11  )
    12  
    13  var gcpIAMPolicyBindingsRoles = []string{
    14  	"roles/compute.viewer",
    15  	"roles/iap.tunnelResourceAccessor",
    16  	"roles/monitoring.viewer",
    17  }
    18  
    19  const opsServiceAccountEmail = "knada-vm-ops-agent@knada-gcp.iam.gserviceaccount.com"
    20  
    21  func (c Client) createComputeInstanceInGCP(ctx context.Context, instanceName, email string) error {
    22  	if c.dryRun {
    23  		return nil
    24  	}
    25  
    26  	exists, err := c.computeInstanceExistsInGCP(ctx, instanceName)
    27  	if err != nil {
    28  		return err
    29  	}
    30  
    31  	if exists {
    32  		return nil
    33  	}
    34  
    35  	cmd := exec.CommandContext(ctx,
    36  		"gcloud",
    37  		"--quiet",
    38  		"compute",
    39  		"instances",
    40  		"create",
    41  		instanceName,
    42  		"--project", c.gcpProject,
    43  		"--zone", c.gcpZone,
    44  		"--machine-type", "n2-standard-2",
    45  		"--network-interface", "network=knada-vpc,subnet=knada,no-address",
    46  		fmt.Sprintf("--labels=goog-ops-agent-policy=v2-x86-template-1-2-0,created-by=knorten,user=%v", normalizeEmailToName(email)),
    47  		"--tags=knadavm",
    48  		"--metadata=block-project-ssh-keys=TRUE,enable-osconfig=TRUE",
    49  		"--service-account", opsServiceAccountEmail,
    50  		"--no-scopes",
    51  	)
    52  
    53  	stdOut := &bytes.Buffer{}
    54  	stdErr := &bytes.Buffer{}
    55  	cmd.Stdout = stdOut
    56  	cmd.Stderr = stdErr
    57  	if err := cmd.Run(); err != nil {
    58  		return fmt.Errorf("%v\nstderr: %v", err, stdErr.String())
    59  	}
    60  
    61  	return nil
    62  }
    63  
    64  func (c Client) resizeComputeInstanceDiskGCP(ctx context.Context, instanceName string, diskSize int32) error {
    65  	if c.dryRun {
    66  		return nil
    67  	}
    68  
    69  	exists, err := c.computeInstanceExistsInGCP(ctx, instanceName)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	if !exists {
    75  		return nil
    76  	}
    77  
    78  	cmd := exec.CommandContext(ctx,
    79  		"gcloud",
    80  		"--quiet",
    81  		"compute",
    82  		"disks",
    83  		"resize",
    84  		instanceName,
    85  		fmt.Sprintf("--project=%v", c.gcpProject),
    86  		fmt.Sprintf("--zone=%v", c.gcpZone),
    87  		fmt.Sprintf("--size=%vGB", diskSize),
    88  	)
    89  
    90  	stdOut := &bytes.Buffer{}
    91  	stdErr := &bytes.Buffer{}
    92  	cmd.Stdout = stdOut
    93  	cmd.Stderr = stdErr
    94  	if err := cmd.Run(); err != nil {
    95  		return fmt.Errorf("%v\nstderr: %v", err, stdErr.String())
    96  	}
    97  
    98  	return nil
    99  }
   100  
   101  func (c Client) deleteComputeInstanceFromGCP(ctx context.Context, instanceName string) error {
   102  	if c.dryRun {
   103  		return nil
   104  	}
   105  
   106  	exists, err := c.computeInstanceExistsInGCP(ctx, instanceName)
   107  	if err != nil {
   108  		return err
   109  	}
   110  
   111  	if !exists {
   112  		return nil
   113  	}
   114  
   115  	cmd := exec.CommandContext(ctx,
   116  		"gcloud",
   117  		"--quiet",
   118  		"compute",
   119  		"instances",
   120  		"delete",
   121  		"--delete-disks=all",
   122  		instanceName,
   123  		"--zone", c.gcpZone,
   124  		"--project", c.gcpProject,
   125  	)
   126  
   127  	stdOut := &bytes.Buffer{}
   128  	stdErr := &bytes.Buffer{}
   129  	cmd.Stdout = stdOut
   130  	cmd.Stderr = stdErr
   131  	if err := cmd.Run(); err != nil {
   132  		return fmt.Errorf("%v\nstderr: %v", err, stdErr.String())
   133  	}
   134  
   135  	return nil
   136  }
   137  
   138  func (c Client) computeInstanceExistsInGCP(ctx context.Context, instanceName string) (bool, error) {
   139  	cmd := exec.CommandContext(ctx,
   140  		"gcloud",
   141  		"--quiet",
   142  		"compute",
   143  		"instances",
   144  		"list",
   145  		"--format=get(name)",
   146  		"--project", c.gcpProject,
   147  		fmt.Sprintf("--filter=name:%v", instanceName))
   148  
   149  	stdOut := &bytes.Buffer{}
   150  	stdErr := &bytes.Buffer{}
   151  	cmd.Stdout = stdOut
   152  	cmd.Stderr = stdErr
   153  	if err := cmd.Run(); err != nil {
   154  		return false, fmt.Errorf("%v\nstderr: %v", err, stdErr.String())
   155  	}
   156  
   157  	return stdOut.String() != "", nil
   158  }
   159  
   160  func (c Client) createIAMPolicyBindingsInGCP(ctx context.Context, instanceName, email string) error {
   161  	if c.dryRun {
   162  		return nil
   163  	}
   164  
   165  	if err := c.addComputeInstanceOwnerBindingInGCP(ctx, instanceName, email); err != nil {
   166  		return err
   167  	}
   168  
   169  	if err := c.addOpsServiceAccountUserBinding(ctx, email); err != nil {
   170  		return err
   171  	}
   172  
   173  	for _, role := range gcpIAMPolicyBindingsRoles {
   174  		if err := c.addProjectIAMPolicyBindingInGCP(ctx, instanceName, email, role); err != nil {
   175  			return err
   176  		}
   177  	}
   178  
   179  	return nil
   180  }
   181  
   182  func (c Client) deleteIAMPolicyBindingsFromGCP(ctx context.Context, instanceName, email string) error {
   183  	if c.dryRun {
   184  		return nil
   185  	}
   186  
   187  	if err := c.removeOpsServiceAccountUserBinding(ctx, email); err != nil {
   188  		return err
   189  	}
   190  
   191  	for _, role := range gcpIAMPolicyBindingsRoles {
   192  		if err := c.removeProjectIAMPolicyBindingFromGCP(ctx, instanceName, email, role); err != nil {
   193  			return err
   194  		}
   195  	}
   196  
   197  	return nil
   198  }
   199  
   200  func (c Client) addComputeInstanceOwnerBindingInGCP(ctx context.Context, instanceName, user string) error {
   201  	cmd := exec.CommandContext(ctx,
   202  		"gcloud",
   203  		"--quiet",
   204  		"compute",
   205  		"instances",
   206  		"add-iam-policy-binding",
   207  		instanceName,
   208  		"--zone", c.gcpZone,
   209  		"--project", c.gcpProject,
   210  		"--role", "roles/owner",
   211  		fmt.Sprintf("--member=user:%v", user),
   212  	)
   213  
   214  	stdOut := &bytes.Buffer{}
   215  	stdErr := &bytes.Buffer{}
   216  	cmd.Stdout = stdOut
   217  	cmd.Stderr = stdErr
   218  	if err := cmd.Run(); err != nil {
   219  		return fmt.Errorf("%v\nstderr: %v", err, stdErr.String())
   220  	}
   221  
   222  	return nil
   223  }
   224  
   225  func (c Client) addOpsServiceAccountUserBinding(ctx context.Context, email string) error {
   226  	cmd := exec.CommandContext(ctx,
   227  		"gcloud",
   228  		"--quiet",
   229  		"iam",
   230  		"service-accounts",
   231  		"add-iam-policy-binding",
   232  		opsServiceAccountEmail,
   233  		"--project", c.gcpProject,
   234  		"--role", "roles/iam.serviceAccountUser",
   235  		fmt.Sprintf("--member=user:%v", email),
   236  	)
   237  
   238  	stdOut := &bytes.Buffer{}
   239  	stdErr := &bytes.Buffer{}
   240  	cmd.Stdout = stdOut
   241  	cmd.Stderr = stdErr
   242  	if err := cmd.Run(); err != nil {
   243  		return fmt.Errorf("%v\nstderr: %v", err, stdErr.String())
   244  	}
   245  
   246  	return nil
   247  }
   248  
   249  func (c Client) removeOpsServiceAccountUserBinding(ctx context.Context, email string) error {
   250  	cmd := exec.CommandContext(ctx,
   251  		"gcloud",
   252  		"--quiet",
   253  		"iam",
   254  		"service-accounts",
   255  		"remove-iam-policy-binding",
   256  		opsServiceAccountEmail,
   257  		"--project", c.gcpProject,
   258  		"--role", "roles/iam.serviceAccountUser",
   259  		fmt.Sprintf("--member=user:%v", email),
   260  	)
   261  
   262  	stdOut := &bytes.Buffer{}
   263  	stdErr := &bytes.Buffer{}
   264  	cmd.Stdout = stdOut
   265  	cmd.Stderr = stdErr
   266  	if err := cmd.Run(); err != nil {
   267  		return fmt.Errorf("%v\nstderr: %v", err, stdErr.String())
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  func (c Client) addProjectIAMPolicyBindingInGCP(ctx context.Context, instanceName, user, role string) error {
   274  	if c.dryRun {
   275  		return nil
   276  	}
   277  
   278  	cmd := exec.CommandContext(ctx,
   279  		"gcloud",
   280  		"--quiet",
   281  		"projects",
   282  		"add-iam-policy-binding",
   283  		c.gcpProject,
   284  		"--role", role,
   285  		fmt.Sprintf("--member=user:%v", user),
   286  	)
   287  
   288  	stdOut := &bytes.Buffer{}
   289  	stdErr := &bytes.Buffer{}
   290  	cmd.Stdout = stdOut
   291  	cmd.Stderr = stdErr
   292  	if err := cmd.Run(); err != nil {
   293  		return fmt.Errorf("%v\nstderr: %v", err, stdErr.String())
   294  	}
   295  
   296  	return nil
   297  }
   298  
   299  func (c Client) removeProjectIAMPolicyBindingFromGCP(ctx context.Context, instanceName, user, role string) error {
   300  	if c.dryRun {
   301  		return nil
   302  	}
   303  
   304  	cmd := exec.CommandContext(ctx,
   305  		"gcloud",
   306  		"--quiet",
   307  		"projects",
   308  		"remove-iam-policy-binding",
   309  		c.gcpProject,
   310  		"--role", role,
   311  		fmt.Sprintf("--member=user:%v", user),
   312  	)
   313  
   314  	stdOut := &bytes.Buffer{}
   315  	stdErr := &bytes.Buffer{}
   316  	cmd.Stdout = stdOut
   317  	cmd.Stderr = stdErr
   318  	if err := cmd.Run(); err != nil {
   319  		if strings.Contains(stdErr.String(), "not found") {
   320  			return nil
   321  		}
   322  
   323  		return fmt.Errorf("%v\nstderr: %v", err, stdErr.String())
   324  	}
   325  
   326  	return nil
   327  }
   328  
   329  func normalizeEmailToName(email string) string {
   330  	name, _ := strings.CutSuffix(email, "@nav.no")
   331  	return strings.ReplaceAll(name, ".", "_")
   332  }
   333  
   334  func (c Client) createUserGSMInGCP(ctx context.Context, name, owner string) error {
   335  	if c.dryRun {
   336  		return nil
   337  	}
   338  
   339  	secret, err := gcp.CreateSecret(ctx, c.gcpProject, c.gcpRegion, name, map[string]string{"owner": name})
   340  	if err != nil {
   341  		return err
   342  	}
   343  
   344  	if err := gcp.SetUsersSecretOwnerBinding(ctx, []string{owner}, secret.Name); err != nil {
   345  		return err
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  func (c Client) deleteUserGSMFromGCP(ctx context.Context, name string) error {
   352  	if c.dryRun {
   353  		return nil
   354  	}
   355  
   356  	if err := gcp.DeleteSecret(ctx, c.gcpProject, name); err != nil {
   357  		return err
   358  	}
   359  
   360  	return nil
   361  }