mirror of https://github.com/kubernetes/kops.git
GCE: Project IAM Binding task
This allows us to grant a project-level permission to a service account.
This commit is contained in:
parent
0cecb07d90
commit
faeeb1fe80
|
|
@ -6,6 +6,7 @@ go_library(
|
||||||
importpath = "k8s.io/kops/cloudmock/gce",
|
importpath = "k8s.io/kops/cloudmock/gce",
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//cloudmock/gce/mockcloudresourcemanager:go_default_library",
|
||||||
"//cloudmock/gce/mockcompute:go_default_library",
|
"//cloudmock/gce/mockcompute:go_default_library",
|
||||||
"//cloudmock/gce/mockdns:go_default_library",
|
"//cloudmock/gce/mockdns:go_default_library",
|
||||||
"//cloudmock/gce/mockiam:go_default_library",
|
"//cloudmock/gce/mockiam:go_default_library",
|
||||||
|
|
@ -16,6 +17,7 @@ go_library(
|
||||||
"//pkg/cloudinstances:go_default_library",
|
"//pkg/cloudinstances:go_default_library",
|
||||||
"//upup/pkg/fi:go_default_library",
|
"//upup/pkg/fi:go_default_library",
|
||||||
"//upup/pkg/fi/cloudup/gce:go_default_library",
|
"//upup/pkg/fi/cloudup/gce:go_default_library",
|
||||||
|
"//vendor/google.golang.org/api/cloudresourcemanager/v1:go_default_library",
|
||||||
"//vendor/google.golang.org/api/compute/v1:go_default_library",
|
"//vendor/google.golang.org/api/compute/v1:go_default_library",
|
||||||
"//vendor/google.golang.org/api/iam/v1:go_default_library",
|
"//vendor/google.golang.org/api/iam/v1:go_default_library",
|
||||||
"//vendor/google.golang.org/api/storage/v1:go_default_library",
|
"//vendor/google.golang.org/api/storage/v1:go_default_library",
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,13 @@ package gce
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"google.golang.org/api/cloudresourcemanager/v1"
|
||||||
compute "google.golang.org/api/compute/v1"
|
compute "google.golang.org/api/compute/v1"
|
||||||
"google.golang.org/api/iam/v1"
|
"google.golang.org/api/iam/v1"
|
||||||
"google.golang.org/api/storage/v1"
|
"google.golang.org/api/storage/v1"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
"k8s.io/kops/cloudmock/gce/mockcloudresourcemanager"
|
||||||
mockcompute "k8s.io/kops/cloudmock/gce/mockcompute"
|
mockcompute "k8s.io/kops/cloudmock/gce/mockcompute"
|
||||||
"k8s.io/kops/cloudmock/gce/mockdns"
|
"k8s.io/kops/cloudmock/gce/mockdns"
|
||||||
"k8s.io/kops/cloudmock/gce/mockiam"
|
"k8s.io/kops/cloudmock/gce/mockiam"
|
||||||
|
|
@ -42,10 +44,11 @@ type MockGCECloud struct {
|
||||||
region string
|
region string
|
||||||
labels map[string]string
|
labels map[string]string
|
||||||
|
|
||||||
computeClient *mockcompute.MockClient
|
computeClient *mockcompute.MockClient
|
||||||
dnsClient *mockdns.MockClient
|
dnsClient *mockdns.MockClient
|
||||||
iamClient *iam.Service
|
iamClient *iam.Service
|
||||||
storageClient *storage.Service
|
storageClient *storage.Service
|
||||||
|
cloudResourceManagerClient *cloudresourcemanager.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ gce.GCECloud = &MockGCECloud{}
|
var _ gce.GCECloud = &MockGCECloud{}
|
||||||
|
|
@ -53,12 +56,13 @@ var _ gce.GCECloud = &MockGCECloud{}
|
||||||
// InstallMockGCECloud registers a MockGCECloud implementation for the specified region & project
|
// InstallMockGCECloud registers a MockGCECloud implementation for the specified region & project
|
||||||
func InstallMockGCECloud(region string, project string) *MockGCECloud {
|
func InstallMockGCECloud(region string, project string) *MockGCECloud {
|
||||||
c := &MockGCECloud{
|
c := &MockGCECloud{
|
||||||
project: project,
|
project: project,
|
||||||
region: region,
|
region: region,
|
||||||
computeClient: mockcompute.NewMockClient(project),
|
computeClient: mockcompute.NewMockClient(project),
|
||||||
dnsClient: mockdns.NewMockClient(),
|
dnsClient: mockdns.NewMockClient(),
|
||||||
iamClient: mockiam.New(project),
|
iamClient: mockiam.New(project),
|
||||||
storageClient: mockstorage.New(),
|
storageClient: mockstorage.New(),
|
||||||
|
cloudResourceManagerClient: mockcloudresourcemanager.New(),
|
||||||
}
|
}
|
||||||
gce.CacheGCECloudInstance(region, project, c)
|
gce.CacheGCECloudInstance(region, project, c)
|
||||||
return c
|
return c
|
||||||
|
|
@ -117,6 +121,11 @@ func (c *MockGCECloud) IAM() *iam.Service {
|
||||||
return c.iamClient
|
return c.iamClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloudResourceManager returns the client for the cloudresourcemanager API
|
||||||
|
func (c *MockGCECloud) CloudResourceManager() *cloudresourcemanager.Service {
|
||||||
|
return c.cloudResourceManagerClient
|
||||||
|
}
|
||||||
|
|
||||||
// CloudDNS returns the DNS client
|
// CloudDNS returns the DNS client
|
||||||
func (c *MockGCECloud) CloudDNS() gce.DNSClient {
|
func (c *MockGCECloud) CloudDNS() gce.DNSClient {
|
||||||
return c.dnsClient
|
return c.dnsClient
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = [
|
||||||
|
"api.go",
|
||||||
|
"projects.go",
|
||||||
|
],
|
||||||
|
importpath = "k8s.io/kops/cloudmock/gce/mockcloudresourcemanager",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//cloudmock/gce/gcphttp:go_default_library",
|
||||||
|
"//vendor/google.golang.org/api/cloudresourcemanager/v1:go_default_library",
|
||||||
|
"//vendor/google.golang.org/api/option:go_default_library",
|
||||||
|
"//vendor/k8s.io/klog/v2:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mockcloudresourcemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"google.golang.org/api/cloudresourcemanager/v1"
|
||||||
|
option "google.golang.org/api/option"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockCloudResourceManagerService represents a mocked cloudresourcemanager client.
|
||||||
|
type mockCloudResourceManagerService struct {
|
||||||
|
svc *cloudresourcemanager.Service
|
||||||
|
|
||||||
|
projects projects
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new mock cloudresourcemanager client.
|
||||||
|
func New() *cloudresourcemanager.Service {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
s := &mockCloudResourceManagerService{}
|
||||||
|
|
||||||
|
s.projects.Init()
|
||||||
|
|
||||||
|
httpClient := &http.Client{Transport: s}
|
||||||
|
svc, err := cloudresourcemanager.NewService(ctx, option.WithHTTPClient(httpClient))
|
||||||
|
if err != nil {
|
||||||
|
klog.Fatalf("failed to build mock cloudresourcemanager service: %v", err)
|
||||||
|
}
|
||||||
|
s.svc = svc
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockCloudResourceManagerService) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
url := request.URL
|
||||||
|
if url.Host != "cloudresourcemanager.googleapis.com" {
|
||||||
|
return nil, fmt.Errorf("unexpected host in request %#v", request)
|
||||||
|
}
|
||||||
|
|
||||||
|
pathTokens := strings.Split(strings.TrimPrefix(url.Path, "/"), "/")
|
||||||
|
if len(pathTokens) >= 1 && pathTokens[0] == "v1" {
|
||||||
|
if len(pathTokens) >= 3 && pathTokens[1] == "projects" {
|
||||||
|
projectTokens := strings.Split(pathTokens[2], ":")
|
||||||
|
if len(projectTokens) == 2 {
|
||||||
|
projectID := projectTokens[0]
|
||||||
|
verb := projectTokens[1]
|
||||||
|
|
||||||
|
if request.Method == "POST" && verb == "getIamPolicy" {
|
||||||
|
return s.projects.getIAMPolicy(projectID, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.Method == "POST" && verb == "setIamPolicy" {
|
||||||
|
return s.projects.setIAMPolicy(projectID, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// klog.Warningf("request: %s %s %#v", request.Method, request.URL, request)
|
||||||
|
return nil, fmt.Errorf("unhandled request %#v", request)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mockcloudresourcemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"google.golang.org/api/cloudresourcemanager/v1"
|
||||||
|
"k8s.io/kops/cloudmock/gce/gcphttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type projects struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
|
||||||
|
projectBindings map[string]*cloudresourcemanager.Policy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *projects) Init() {
|
||||||
|
s.projectBindings = make(map[string]*cloudresourcemanager.Policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *projects) getIAMPolicy(projectID string, request *http.Request) (*http.Response, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
bindings := s.projectBindings[projectID]
|
||||||
|
if bindings == nil {
|
||||||
|
bindings = &cloudresourcemanager.Policy{
|
||||||
|
Etag: nextEtag(""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcphttp.OKResponse(bindings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextEtag(etag string) string {
|
||||||
|
hash := sha256.Sum256([]byte(etag))
|
||||||
|
nextEtag := hex.EncodeToString(hash[:])
|
||||||
|
return nextEtag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *projects) setIAMPolicy(projectID string, request *http.Request) (*http.Response, error) {
|
||||||
|
b, err := io.ReadAll(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return gcphttp.ErrorBadRequest("")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &cloudresourcemanager.SetIamPolicyRequest{}
|
||||||
|
if err := json.Unmarshal(b, &req); err != nil {
|
||||||
|
return gcphttp.ErrorBadRequest("")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
oldBindings := s.projectBindings[projectID]
|
||||||
|
if oldBindings == nil {
|
||||||
|
oldBindings = &cloudresourcemanager.Policy{
|
||||||
|
Etag: nextEtag(""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newBindings := req.Policy
|
||||||
|
|
||||||
|
if oldBindings.Etag != newBindings.Etag {
|
||||||
|
// TODO: What is the actual error?
|
||||||
|
return gcphttp.ErrorNotFound("etag")
|
||||||
|
}
|
||||||
|
|
||||||
|
newBindings.Etag = nextEtag(oldBindings.Etag)
|
||||||
|
s.projectBindings[projectID] = newBindings
|
||||||
|
|
||||||
|
return gcphttp.OKResponse(newBindings)
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ go_library(
|
||||||
"//upup/pkg/fi:go_default_library",
|
"//upup/pkg/fi:go_default_library",
|
||||||
"//upup/pkg/fi/cloudup/gce/gcemetadata:go_default_library",
|
"//upup/pkg/fi/cloudup/gce/gcemetadata:go_default_library",
|
||||||
"//vendor/golang.org/x/oauth2/google:go_default_library",
|
"//vendor/golang.org/x/oauth2/google:go_default_library",
|
||||||
|
"//vendor/google.golang.org/api/cloudresourcemanager/v1:go_default_library",
|
||||||
"//vendor/google.golang.org/api/compute/v1:go_default_library",
|
"//vendor/google.golang.org/api/compute/v1:go_default_library",
|
||||||
"//vendor/google.golang.org/api/dns/v1:go_default_library",
|
"//vendor/google.golang.org/api/dns/v1:go_default_library",
|
||||||
"//vendor/google.golang.org/api/googleapi:go_default_library",
|
"//vendor/google.golang.org/api/googleapi:go_default_library",
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
|
"google.golang.org/api/cloudresourcemanager/v1"
|
||||||
compute "google.golang.org/api/compute/v1"
|
compute "google.golang.org/api/compute/v1"
|
||||||
"google.golang.org/api/iam/v1"
|
"google.golang.org/api/iam/v1"
|
||||||
oauth2 "google.golang.org/api/oauth2/v2"
|
oauth2 "google.golang.org/api/oauth2/v2"
|
||||||
|
|
@ -50,6 +51,9 @@ type GCECloud interface {
|
||||||
|
|
||||||
// ServiceAccount returns the email for the service account that the instances will run under
|
// ServiceAccount returns the email for the service account that the instances will run under
|
||||||
ServiceAccount() (string, error)
|
ServiceAccount() (string, error)
|
||||||
|
|
||||||
|
// CloudResourceManager returns the client for the cloudresourcemanager API
|
||||||
|
CloudResourceManager() *cloudresourcemanager.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
type gceCloudImplementation struct {
|
type gceCloudImplementation struct {
|
||||||
|
|
@ -58,6 +62,9 @@ type gceCloudImplementation struct {
|
||||||
iam *iam.Service
|
iam *iam.Service
|
||||||
dns *dnsClientImpl
|
dns *dnsClientImpl
|
||||||
|
|
||||||
|
// cloudResourceManager is the client for the cloudresourcemanager API
|
||||||
|
cloudResourceManager *cloudresourcemanager.Service
|
||||||
|
|
||||||
region string
|
region string
|
||||||
project string
|
project string
|
||||||
|
|
||||||
|
|
@ -144,6 +151,12 @@ func NewGCECloud(region string, project string, labels map[string]string) (GCECl
|
||||||
}
|
}
|
||||||
c.dns = dnsClient
|
c.dns = dnsClient
|
||||||
|
|
||||||
|
cloudResourceManager, err := cloudresourcemanager.NewService(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error building cloudresourcemanager API client: %w", err)
|
||||||
|
}
|
||||||
|
c.cloudResourceManager = cloudResourceManager
|
||||||
|
|
||||||
CacheGCECloudInstance(region, project, c)
|
CacheGCECloudInstance(region, project, c)
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -193,11 +206,16 @@ func (c *gceCloudImplementation) IAM() *iam.Service {
|
||||||
return c.iam
|
return c.iam
|
||||||
}
|
}
|
||||||
|
|
||||||
// NameService returns the DNS client
|
// CloudDNS returns the DNS client
|
||||||
func (c *gceCloudImplementation) CloudDNS() DNSClient {
|
func (c *gceCloudImplementation) CloudDNS() DNSClient {
|
||||||
return c.dns
|
return c.dns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloudResourceManager returns the client for the cloudresourcemanager API
|
||||||
|
func (c *gceCloudImplementation) CloudResourceManager() *cloudresourcemanager.Service {
|
||||||
|
return c.cloudResourceManager
|
||||||
|
}
|
||||||
|
|
||||||
// Region returns private struct element region.
|
// Region returns private struct element region.
|
||||||
func (c *gceCloudImplementation) Region() string {
|
func (c *gceCloudImplementation) Region() string {
|
||||||
return c.region
|
return c.region
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ go_library(
|
||||||
"instancetemplate_fitask.go",
|
"instancetemplate_fitask.go",
|
||||||
"network.go",
|
"network.go",
|
||||||
"network_fitask.go",
|
"network_fitask.go",
|
||||||
|
"projectiambinding.go",
|
||||||
|
"projectiambinding_fitask.go",
|
||||||
"router.go",
|
"router.go",
|
||||||
"router_fitask.go",
|
"router_fitask.go",
|
||||||
"serviceaccount.go",
|
"serviceaccount.go",
|
||||||
|
|
@ -43,6 +45,7 @@ go_library(
|
||||||
"//upup/pkg/fi/cloudup/gce:go_default_library",
|
"//upup/pkg/fi/cloudup/gce:go_default_library",
|
||||||
"//upup/pkg/fi/cloudup/terraform:go_default_library",
|
"//upup/pkg/fi/cloudup/terraform:go_default_library",
|
||||||
"//upup/pkg/fi/cloudup/terraformWriter:go_default_library",
|
"//upup/pkg/fi/cloudup/terraformWriter:go_default_library",
|
||||||
|
"//vendor/google.golang.org/api/cloudresourcemanager/v1:go_default_library",
|
||||||
"//vendor/google.golang.org/api/compute/v1:go_default_library",
|
"//vendor/google.golang.org/api/compute/v1:go_default_library",
|
||||||
"//vendor/google.golang.org/api/iam/v1:go_default_library",
|
"//vendor/google.golang.org/api/iam/v1:go_default_library",
|
||||||
"//vendor/google.golang.org/api/storage/v1:go_default_library",
|
"//vendor/google.golang.org/api/storage/v1:go_default_library",
|
||||||
|
|
@ -53,6 +56,7 @@ go_library(
|
||||||
go_test(
|
go_test(
|
||||||
name = "go_default_test",
|
name = "go_default_test",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"projectiambinding_test.go",
|
||||||
"serviceaccount_test.go",
|
"serviceaccount_test.go",
|
||||||
"storagebucketiam_test.go",
|
"storagebucketiam_test.go",
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gcetasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"google.golang.org/api/cloudresourcemanager/v1"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
"k8s.io/kops/upup/pkg/fi"
|
||||||
|
"k8s.io/kops/upup/pkg/fi/cloudup/gce"
|
||||||
|
"k8s.io/kops/upup/pkg/fi/cloudup/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProjectIAMBinding represents an IAM rule on a project
|
||||||
|
// +kops:fitask
|
||||||
|
type ProjectIAMBinding struct {
|
||||||
|
Name *string
|
||||||
|
Lifecycle fi.Lifecycle
|
||||||
|
|
||||||
|
Project *string
|
||||||
|
Member *string
|
||||||
|
Role *string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fi.CompareWithID = &ProjectIAMBinding{}
|
||||||
|
|
||||||
|
func (e *ProjectIAMBinding) CompareWithID() *string {
|
||||||
|
return e.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ProjectIAMBinding) Find(c *fi.Context) (*ProjectIAMBinding, error) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
cloud := c.Cloud.(gce.GCECloud)
|
||||||
|
|
||||||
|
projectID := fi.StringValue(e.Project)
|
||||||
|
member := fi.StringValue(e.Member)
|
||||||
|
role := fi.StringValue(e.Role)
|
||||||
|
|
||||||
|
klog.V(2).Infof("Checking IAM for project %q", projectID)
|
||||||
|
options := &cloudresourcemanager.GetIamPolicyRequest{Options: &cloudresourcemanager.GetPolicyOptions{RequestedPolicyVersion: 3}}
|
||||||
|
policy, err := cloud.CloudResourceManager().Projects.GetIamPolicy(projectID, options).Context(ctx).Do()
|
||||||
|
if err != nil {
|
||||||
|
if gce.IsNotFound(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("error checking IAM for project %s: %w", projectID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
changed := patchCRMPolicy(policy, member, role)
|
||||||
|
if changed {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := &ProjectIAMBinding{}
|
||||||
|
actual.Project = e.Project
|
||||||
|
actual.Member = e.Member
|
||||||
|
actual.Role = e.Role
|
||||||
|
|
||||||
|
// Ignore "system" fields
|
||||||
|
actual.Name = e.Name
|
||||||
|
actual.Lifecycle = e.Lifecycle
|
||||||
|
|
||||||
|
return actual, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ProjectIAMBinding) Run(c *fi.Context) error {
|
||||||
|
return fi.DefaultDeltaRunMethod(e, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ *ProjectIAMBinding) CheckChanges(a, e, changes *ProjectIAMBinding) error {
|
||||||
|
if fi.StringValue(e.Project) == "" {
|
||||||
|
return fi.RequiredField("Project")
|
||||||
|
}
|
||||||
|
if fi.StringValue(e.Member) == "" {
|
||||||
|
return fi.RequiredField("Member")
|
||||||
|
}
|
||||||
|
if fi.StringValue(e.Role) == "" {
|
||||||
|
return fi.RequiredField("Role")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ *ProjectIAMBinding) RenderGCE(t *gce.GCEAPITarget, a, e, changes *ProjectIAMBinding) error {
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
projectID := fi.StringValue(e.Project)
|
||||||
|
member := fi.StringValue(e.Member)
|
||||||
|
role := fi.StringValue(e.Role)
|
||||||
|
|
||||||
|
request := &cloudresourcemanager.GetIamPolicyRequest{}
|
||||||
|
policy, err := t.Cloud.CloudResourceManager().Projects.GetIamPolicy(projectID, request).Context(ctx).Do()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting IAM policy for project %s: %w", projectID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
changed := patchCRMPolicy(policy, member, role)
|
||||||
|
|
||||||
|
if !changed {
|
||||||
|
klog.Warningf("did not need to change policy (concurrent change?)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
klog.V(2).Infof("updating IAM for project %s", projectID)
|
||||||
|
if _, err := t.Cloud.CloudResourceManager().Projects.SetIamPolicy(projectID, &cloudresourcemanager.SetIamPolicyRequest{Policy: policy}).Context(ctx).Do(); err != nil {
|
||||||
|
return fmt.Errorf("error updating IAM for project %s: %w", projectID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// terraformProjectIAMBinding is the model for a terraform google_project_iam_binding rule
|
||||||
|
type terraformProjectIAMBinding struct {
|
||||||
|
Project string `json:"project,omitempty" cty:"project"`
|
||||||
|
Role string `json:"role,omitempty" cty:"role"`
|
||||||
|
Member string `json:"member,omitempty" cty:"member"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ *ProjectIAMBinding) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *ProjectIAMBinding) error {
|
||||||
|
tf := &terraformProjectIAMBinding{
|
||||||
|
Project: fi.StringValue(e.Project),
|
||||||
|
Role: fi.StringValue(e.Role),
|
||||||
|
Member: fi.StringValue(e.Member),
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.RenderResource("google_project_iam_binding", *e.Name, tf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchCRMPolicy(policy *cloudresourcemanager.Policy, wantMember string, wantRole string) bool {
|
||||||
|
for _, binding := range policy.Bindings {
|
||||||
|
if binding.Condition != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if binding.Role != wantRole {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
exists := false
|
||||||
|
for _, member := range binding.Members {
|
||||||
|
if member == wantMember {
|
||||||
|
exists = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
binding.Members = append(binding.Members, wantMember)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
policy.Bindings = append(policy.Bindings, &cloudresourcemanager.Binding{
|
||||||
|
Members: []string{wantMember},
|
||||||
|
Role: wantRole,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
//go:build !ignore_autogenerated
|
||||||
|
// +build !ignore_autogenerated
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by fitask. DO NOT EDIT.
|
||||||
|
|
||||||
|
package gcetasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/kops/upup/pkg/fi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProjectIAMBinding
|
||||||
|
|
||||||
|
var _ fi.HasLifecycle = &ProjectIAMBinding{}
|
||||||
|
|
||||||
|
// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle
|
||||||
|
func (o *ProjectIAMBinding) GetLifecycle() fi.Lifecycle {
|
||||||
|
return o.Lifecycle
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLifecycle sets the Lifecycle of the object, implementing fi.SetLifecycle
|
||||||
|
func (o *ProjectIAMBinding) SetLifecycle(lifecycle fi.Lifecycle) {
|
||||||
|
o.Lifecycle = lifecycle
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fi.HasName = &ProjectIAMBinding{}
|
||||||
|
|
||||||
|
// GetName returns the Name of the object, implementing fi.HasName
|
||||||
|
func (o *ProjectIAMBinding) GetName() *string {
|
||||||
|
return o.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is the stringer function for the task, producing readable output using fi.TaskAsString
|
||||||
|
func (o *ProjectIAMBinding) String() string {
|
||||||
|
return fi.TaskAsString(o)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gcetasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gcemock "k8s.io/kops/cloudmock/gce"
|
||||||
|
"k8s.io/kops/upup/pkg/fi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProjectIAMBinding(t *testing.T) {
|
||||||
|
project := "testproject"
|
||||||
|
region := "us-test1"
|
||||||
|
|
||||||
|
cloud := gcemock.InstallMockGCECloud(region, project)
|
||||||
|
|
||||||
|
// We define a function so we can rebuild the tasks, because we modify in-place when running
|
||||||
|
buildTasks := func() map[string]fi.Task {
|
||||||
|
binding := &ProjectIAMBinding{
|
||||||
|
Lifecycle: fi.LifecycleSync,
|
||||||
|
|
||||||
|
Project: fi.String("testproject"),
|
||||||
|
Member: fi.String("serviceAccount:foo@testproject.iam.gserviceaccount.com"),
|
||||||
|
Role: fi.String("roles/owner"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]fi.Task{
|
||||||
|
"binding": binding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
allTasks := buildTasks()
|
||||||
|
checkHasChanges(t, cloud, allTasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
allTasks := buildTasks()
|
||||||
|
runTasks(t, cloud, allTasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
allTasks := buildTasks()
|
||||||
|
checkNoChanges(t, cloud, allTasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue