diff --git a/cmd/kops/create_cluster.go b/cmd/kops/create_cluster.go index 2d4fa08aaa881..c45bec4b8ac4c 100644 --- a/cmd/kops/create_cluster.go +++ b/cmd/kops/create_cluster.go @@ -996,7 +996,7 @@ func RunCreateCluster(f *util.Factory, out io.Writer, c *CreateClusterOptions) e return err } - err = registry.WriteConfigDeprecated(configBase.Join(registry.PathClusterCompleted), fullCluster) + err = registry.WriteConfigDeprecated(cluster, configBase.Join(registry.PathClusterCompleted), fullCluster) if err != nil { return fmt.Errorf("error writing completed cluster spec: %v", err) } diff --git a/cmd/kops/edit_cluster.go b/cmd/kops/edit_cluster.go index 5a2cb3aa19229..d9a5c2dd92d83 100644 --- a/cmd/kops/edit_cluster.go +++ b/cmd/kops/edit_cluster.go @@ -250,7 +250,7 @@ func RunEditCluster(f *util.Factory, cmd *cobra.Command, args []string, out io.W return preservedFile(err, file, out) } - err = registry.WriteConfigDeprecated(configBase.Join(registry.PathClusterCompleted), fullCluster) + err = registry.WriteConfigDeprecated(newCluster, configBase.Join(registry.PathClusterCompleted), fullCluster) if err != nil { return preservedFile(fmt.Errorf("error writing completed cluster spec: %v", err), file, out) } diff --git a/cmd/kops/util/BUILD.bazel b/cmd/kops/util/BUILD.bazel index acce5f04ab2bf..ba72fc67a4af7 100644 --- a/cmd/kops/util/BUILD.bazel +++ b/cmd/kops/util/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = ["factory.go"], visibility = ["//visibility:public"], deps = [ + "//pkg/acls/gce:go_default_library", "//pkg/client/clientset_generated/clientset:go_default_library", "//pkg/client/simple:go_default_library", "//pkg/client/simple/api:go_default_library", diff --git a/cmd/kops/util/factory.go b/cmd/kops/util/factory.go index 42577ebea020f..cc081046dfc42 100644 --- a/cmd/kops/util/factory.go +++ b/cmd/kops/util/factory.go @@ -18,17 +18,18 @@ package util import ( "fmt" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/kops/pkg/client/simple" - "k8s.io/kops/pkg/client/simple/vfsclientset" - "k8s.io/kops/util/pkg/vfs" + "net/url" + "strings" "github.com/golang/glog" + "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/rest" + gceacls "k8s.io/kops/pkg/acls/gce" kopsclient "k8s.io/kops/pkg/client/clientset_generated/clientset" + "k8s.io/kops/pkg/client/simple" "k8s.io/kops/pkg/client/simple/api" - "net/url" - "strings" + "k8s.io/kops/pkg/client/simple/vfsclientset" + "k8s.io/kops/util/pkg/vfs" ) type FactoryOptions struct { @@ -41,6 +42,8 @@ type Factory struct { } func NewFactory(options *FactoryOptions) *Factory { + gceacls.Register() + return &Factory{ options: options, } diff --git a/federation/apply_federation.go b/federation/apply_federation.go index 4422d4c9c788e..eeee419b1b3d3 100644 --- a/federation/apply_federation.go +++ b/federation/apply_federation.go @@ -207,7 +207,7 @@ func (o *ApplyFederationOperation) federationContextForCluster(cluster *kopsapi. federationKeystore := k8sapi.NewKubernetesKeystore(target.KubernetesClient, o.namespace) checkExisting := true - context, err := fi.NewContext(target, nil, federationKeystore, nil, nil, checkExisting, nil) + context, err := fi.NewContext(target, nil, nil, federationKeystore, nil, nil, checkExisting, nil) if err != nil { return nil, err } diff --git a/hack/.packages b/hack/.packages index 9f7f66209b170..3794101ea8c46 100644 --- a/hack/.packages +++ b/hack/.packages @@ -23,6 +23,8 @@ k8s.io/kops/nodeup/pkg/bootstrap k8s.io/kops/nodeup/pkg/distros k8s.io/kops/nodeup/pkg/model k8s.io/kops/nodeup/pkg/model/resources +k8s.io/kops/pkg/acls +k8s.io/kops/pkg/acls/gce k8s.io/kops/pkg/apis/kops k8s.io/kops/pkg/apis/kops/install k8s.io/kops/pkg/apis/kops/model diff --git a/nodeup/pkg/bootstrap/install.go b/nodeup/pkg/bootstrap/install.go index 561f83d84606b..96bd3f173cf81 100644 --- a/nodeup/pkg/bootstrap/install.go +++ b/nodeup/pkg/bootstrap/install.go @@ -80,7 +80,7 @@ func (i *Installation) Run() error { } checkExisting := true - context, err := fi.NewContext(target, cloud, keyStore, secretStore, configBase, checkExisting, tasks) + context, err := fi.NewContext(target, nil, cloud, keyStore, secretStore, configBase, checkExisting, tasks) if err != nil { return fmt.Errorf("error building context: %v", err) } diff --git a/pkg/acls/BUILD.bazel b/pkg/acls/BUILD.bazel new file mode 100644 index 0000000000000..b0d35754c0b0a --- /dev/null +++ b/pkg/acls/BUILD.bazel @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "interface.go", + "plugins.go", + ], + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/kops:go_default_library", + "//util/pkg/vfs:go_default_library", + ], +) diff --git a/pkg/acls/gce/BUILD.bazel b/pkg/acls/gce/BUILD.bazel new file mode 100644 index 0000000000000..d02aee6a80dfc --- /dev/null +++ b/pkg/acls/gce/BUILD.bazel @@ -0,0 +1,15 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["storage.go"], + visibility = ["//visibility:public"], + deps = [ + "//pkg/acls:go_default_library", + "//pkg/apis/kops:go_default_library", + "//upup/pkg/fi/cloudup:go_default_library", + "//upup/pkg/fi/cloudup/gce:go_default_library", + "//util/pkg/vfs:go_default_library", + "//vendor/google.golang.org/api/storage/v1:go_default_library", + ], +) diff --git a/pkg/acls/gce/storage.go b/pkg/acls/gce/storage.go new file mode 100644 index 0000000000000..24b7b88840e02 --- /dev/null +++ b/pkg/acls/gce/storage.go @@ -0,0 +1,84 @@ +/* +Copyright 2017 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 gce + +import ( + "fmt" + + storage "google.golang.org/api/storage/v1" + "k8s.io/kops/pkg/acls" + "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/upup/pkg/fi/cloudup" + "k8s.io/kops/upup/pkg/fi/cloudup/gce" + "k8s.io/kops/util/pkg/vfs" +) + +// gcsAclStrategy is the AclStrategy for objects written to google cloud storage +type gcsAclStrategy struct { +} + +var _ acls.ACLStrategy = &gcsAclStrategy{} + +// GetACL returns the ACL to use if this is a google cloud storage path +func (s *gcsAclStrategy) GetACL(p vfs.Path, cluster *kops.Cluster) (vfs.ACL, error) { + if kops.CloudProviderID(cluster.Spec.CloudProvider) != kops.CloudProviderGCE { + return nil, nil + } + gcsPath, ok := p.(*vfs.GSPath) + if !ok { + return nil, nil + } + + bucketName := gcsPath.Bucket() + client := gcsPath.Client() + + // TODO: Cache? + bucket, err := client.Buckets.Get(bucketName).Do() + if err != nil { + return nil, fmt.Errorf("error querying bucket %q: %v", bucketName, err) + } + + // TODO: Cache? + cloud, err := cloudup.BuildCloud(cluster) + if err != nil { + return nil, err + } + + serviceAccount, err := cloud.(gce.GCECloud).ServiceAccount() + if err != nil { + return nil, err + } + + var acls []*storage.ObjectAccessControl + for _, a := range bucket.DefaultObjectAcl { + acls = append(acls, a) + } + + acls = append(acls, &storage.ObjectAccessControl{ + Email: serviceAccount, + Entity: "user-" + serviceAccount, + Role: "READER", + }) + + return &vfs.GSAcl{ + Acl: acls, + }, nil +} + +func Register() { + acls.RegisterPlugin("k8s.io/kops/acl/gce", &gcsAclStrategy{}) +} diff --git a/pkg/acls/interface.go b/pkg/acls/interface.go new file mode 100644 index 0000000000000..06773045c7d44 --- /dev/null +++ b/pkg/acls/interface.go @@ -0,0 +1,28 @@ +/* +Copyright 2017 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 acls + +import ( + "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/util/pkg/vfs" +) + +// ACLStrategy is the interface implemented by ACL strategy providers +type ACLStrategy interface { + // GetACL returns the ACL if this strategy handles the vfs.Path, when writing for the specified cluster + GetACL(p vfs.Path, cluster *kops.Cluster) (vfs.ACL, error) +} diff --git a/pkg/acls/plugins.go b/pkg/acls/plugins.go new file mode 100644 index 0000000000000..a8b26bf953699 --- /dev/null +++ b/pkg/acls/plugins.go @@ -0,0 +1,57 @@ +/* +Copyright 2017 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 acls + +import ( + "fmt" + "sync" + + "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/util/pkg/vfs" +) + +var strategies map[string]ACLStrategy +var strategiesMutex sync.Mutex + +// GetACL returns the ACL for the vfs.Path, by consulting all registered strategies +func GetACL(p vfs.Path, cluster *kops.Cluster) (vfs.ACL, error) { + strategiesMutex.Lock() + defer strategiesMutex.Unlock() + + for k, strategy := range strategies { + acl, err := strategy.GetACL(p, cluster) + if err != nil { + return nil, fmt.Errorf("error from acl provider %q: %v", k, err) + } + if acl != nil { + return acl, nil + } + } + return nil, nil +} + +// RegisterPlugin adds the strategy to the registered strategies +func RegisterPlugin(key string, strategy ACLStrategy) { + strategiesMutex.Lock() + defer strategiesMutex.Unlock() + + if strategies == nil { + strategies = make(map[string]ACLStrategy) + } + + strategies[key] = strategy +} diff --git a/pkg/apis/kops/registry/BUILD.bazel b/pkg/apis/kops/registry/BUILD.bazel index c2e5d155721b3..a8bf29ebfbcb7 100644 --- a/pkg/apis/kops/registry/BUILD.bazel +++ b/pkg/apis/kops/registry/BUILD.bazel @@ -9,6 +9,7 @@ go_library( ], visibility = ["//visibility:public"], deps = [ + "//pkg/acls:go_default_library", "//pkg/apis/kops:go_default_library", "//pkg/client/simple:go_default_library", "//upup/pkg/fi/utils:go_default_library", diff --git a/pkg/apis/kops/registry/statestore.go b/pkg/apis/kops/registry/statestore.go index f63013df4daae..49ca895aff1d7 100644 --- a/pkg/apis/kops/registry/statestore.go +++ b/pkg/apis/kops/registry/statestore.go @@ -18,10 +18,13 @@ package registry import ( "fmt" - "k8s.io/kops/upup/pkg/fi/utils" - "k8s.io/kops/util/pkg/vfs" "os" "strings" + + "k8s.io/kops/pkg/acls" + "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/upup/pkg/fi/utils" + "k8s.io/kops/util/pkg/vfs" ) func ReadConfigDeprecated(configPath vfs.Path, config interface{}) error { @@ -49,7 +52,7 @@ func ReadConfigDeprecated(configPath vfs.Path, config interface{}) error { // WriteConfigDeprecated writes a config file as yaml. // It is deprecated because it is unversioned, but it is still used, in particular for writing the completed config. -func WriteConfigDeprecated(configPath vfs.Path, config interface{}, writeOptions ...vfs.WriteOption) error { +func WriteConfigDeprecated(cluster *kops.Cluster, configPath vfs.Path, config interface{}, writeOptions ...vfs.WriteOption) error { data, err := utils.YamlMarshal(config) if err != nil { return fmt.Errorf("error marshalling configuration: %v", err) @@ -73,10 +76,15 @@ func WriteConfigDeprecated(configPath vfs.Path, config interface{}, writeOptions } } + acl, err := acls.GetACL(configPath, cluster) + if err != nil { + return err + } + if create { - err = configPath.CreateFile(data) + err = configPath.CreateFile(data, acl) } else { - err = configPath.WriteFile(data) + err = configPath.WriteFile(data, acl) } if err != nil { return fmt.Errorf("error writing configuration file %s: %v", configPath, err) diff --git a/pkg/client/simple/api/clientset.go b/pkg/client/simple/api/clientset.go index 4054e55648a6d..a800b9c50d345 100644 --- a/pkg/client/simple/api/clientset.go +++ b/pkg/client/simple/api/clientset.go @@ -109,12 +109,12 @@ func (c *RESTClientset) GetFederation(name string) (*kops.Federation, error) { func (c *RESTClientset) SecretStore(cluster *kops.Cluster) (fi.SecretStore, error) { namespace := restNamespaceForClusterName(cluster.Name) - return secrets.NewClientsetSecretStore(c.KopsClient, namespace), nil + return secrets.NewClientsetSecretStore(cluster, c.KopsClient, namespace), nil } func (c *RESTClientset) KeyStore(cluster *kops.Cluster) (fi.CAStore, error) { namespace := restNamespaceForClusterName(cluster.Name) - return fi.NewClientsetCAStore(c.KopsClient, namespace), nil + return fi.NewClientsetCAStore(cluster, c.KopsClient, namespace), nil } func (c *RESTClientset) DeleteCluster(cluster *kops.Cluster) error { diff --git a/pkg/client/simple/vfsclientset/BUILD.bazel b/pkg/client/simple/vfsclientset/BUILD.bazel index 099e45186bb78..639747ced800e 100644 --- a/pkg/client/simple/vfsclientset/BUILD.bazel +++ b/pkg/client/simple/vfsclientset/BUILD.bazel @@ -13,6 +13,7 @@ go_library( ], visibility = ["//visibility:public"], deps = [ + "//pkg/acls:go_default_library", "//pkg/apis/kops:go_default_library", "//pkg/apis/kops/registry:go_default_library", "//pkg/apis/kops/v1alpha1:go_default_library", diff --git a/pkg/client/simple/vfsclientset/clientset.go b/pkg/client/simple/vfsclientset/clientset.go index fc963eae6b3b9..ab0d759fdbdb4 100644 --- a/pkg/client/simple/vfsclientset/clientset.go +++ b/pkg/client/simple/vfsclientset/clientset.go @@ -70,8 +70,7 @@ func (c *VFSClientset) ConfigBaseFor(cluster *kops.Cluster) (vfs.Path, error) { // InstanceGroupsFor implements the InstanceGroupsFor method of simple.Clientset for a VFS-backed state store func (c *VFSClientset) InstanceGroupsFor(cluster *kops.Cluster) kopsinternalversion.InstanceGroupInterface { - clusterName := cluster.Name - return newInstanceGroupVFS(c, clusterName) + return newInstanceGroupVFS(c, cluster) } func (c *VFSClientset) federations() kopsinternalversion.FederationInterface { @@ -99,7 +98,7 @@ func (c *VFSClientset) SecretStore(cluster *kops.Cluster) (fi.SecretStore, error return nil, err } basedir := configBase.Join("secrets") - return secrets.NewVFSSecretStore(basedir), nil + return secrets.NewVFSSecretStore(cluster, basedir), nil } func (c *VFSClientset) KeyStore(cluster *kops.Cluster) (fi.CAStore, error) { @@ -108,7 +107,7 @@ func (c *VFSClientset) KeyStore(cluster *kops.Cluster) (fi.CAStore, error) { return nil, err } basedir := configBase.Join("pki") - return fi.NewVFSCAStore(basedir), nil + return fi.NewVFSCAStore(cluster, basedir), nil } func DeleteAllClusterState(basePath vfs.Path) error { diff --git a/pkg/client/simple/vfsclientset/cluster.go b/pkg/client/simple/vfsclientset/cluster.go index f9327a61f0385..5080e7a73fc8e 100644 --- a/pkg/client/simple/vfsclientset/cluster.go +++ b/pkg/client/simple/vfsclientset/cluster.go @@ -101,7 +101,7 @@ func (r *ClusterVFS) Create(c *api.Cluster) (*api.Cluster, error) { return nil, fmt.Errorf("clusterName is required") } - if err := r.writeConfig(r.basePath.Join(clusterName, registry.PathCluster), c, vfs.WriteOptionCreate); err != nil { + if err := r.writeConfig(c, r.basePath.Join(clusterName, registry.PathCluster), c, vfs.WriteOptionCreate); err != nil { if os.IsExist(err) { return nil, err } @@ -126,7 +126,7 @@ func (r *ClusterVFS) Update(c *api.Cluster, status *api.ClusterStatus) (*api.Clu return nil, err } - if err := r.writeConfig(r.basePath.Join(clusterName, registry.PathCluster), c, vfs.WriteOptionOnlyIfExists); err != nil { + if err := r.writeConfig(c, r.basePath.Join(clusterName, registry.PathCluster), c, vfs.WriteOptionOnlyIfExists); err != nil { if os.IsNotExist(err) { return nil, err } diff --git a/pkg/client/simple/vfsclientset/commonvfs.go b/pkg/client/simple/vfsclientset/commonvfs.go index 875cf4c38818e..92217231ff6e2 100644 --- a/pkg/client/simple/vfsclientset/commonvfs.go +++ b/pkg/client/simple/vfsclientset/commonvfs.go @@ -25,6 +25,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kops/pkg/acls" kops "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/apis/kops/v1alpha2" "k8s.io/kops/pkg/kopscodecs" @@ -76,7 +77,7 @@ func (c *commonVFS) list(items interface{}, options metav1.ListOptions) (interfa return c.readAll(items) } -func (c *commonVFS) create(i runtime.Object) error { +func (c *commonVFS) create(cluster *kops.Cluster, i runtime.Object) error { objectMeta, err := meta.Accessor(i) if err != nil { return err @@ -94,7 +95,7 @@ func (c *commonVFS) create(i runtime.Object) error { objectMeta.SetCreationTimestamp(v1.NewTime(time.Now().UTC())) } - err = c.writeConfig(c.basePath.Join(objectMeta.GetName()), i, vfs.WriteOptionCreate) + err = c.writeConfig(cluster, c.basePath.Join(objectMeta.GetName()), i, vfs.WriteOptionCreate) if err != nil { if os.IsExist(err) { return err @@ -131,7 +132,7 @@ func (c *commonVFS) readConfig(configPath vfs.Path) (runtime.Object, error) { return object, nil } -func (c *commonVFS) writeConfig(configPath vfs.Path, o runtime.Object, writeOptions ...vfs.WriteOption) error { +func (c *commonVFS) writeConfig(cluster *kops.Cluster, configPath vfs.Path, o runtime.Object, writeOptions ...vfs.WriteOption) error { data, err := c.serialize(o) if err != nil { return fmt.Errorf("error marshalling object: %v", err) @@ -155,10 +156,15 @@ func (c *commonVFS) writeConfig(configPath vfs.Path, o runtime.Object, writeOpti } } + acl, err := acls.GetACL(configPath, cluster) + if err != nil { + return err + } + if create { - err = configPath.CreateFile(data) + err = configPath.CreateFile(data, acl) } else { - err = configPath.WriteFile(data) + err = configPath.WriteFile(data, acl) } if err != nil { if create && os.IsExist(err) { @@ -170,7 +176,7 @@ func (c *commonVFS) writeConfig(configPath vfs.Path, o runtime.Object, writeOpti return nil } -func (c *commonVFS) update(i runtime.Object) error { +func (c *commonVFS) update(cluster *kops.Cluster, i runtime.Object) error { objectMeta, err := meta.Accessor(i) if err != nil { return err @@ -188,7 +194,7 @@ func (c *commonVFS) update(i runtime.Object) error { objectMeta.SetCreationTimestamp(v1.NewTime(time.Now().UTC())) } - err = c.writeConfig(c.basePath.Join(objectMeta.GetName()), i, vfs.WriteOptionOnlyIfExists) + err = c.writeConfig(cluster, c.basePath.Join(objectMeta.GetName()), i, vfs.WriteOptionOnlyIfExists) if err != nil { return fmt.Errorf("error writing %s: %v", c.kind, err) } diff --git a/pkg/client/simple/vfsclientset/federation.go b/pkg/client/simple/vfsclientset/federation.go index 8feb48132c0e7..09940c0ac98d5 100644 --- a/pkg/client/simple/vfsclientset/federation.go +++ b/pkg/client/simple/vfsclientset/federation.go @@ -72,7 +72,7 @@ func (c *FederationVFS) List(options metav1.ListOptions) (*api.FederationList, e } func (c *FederationVFS) Create(g *api.Federation) (*api.Federation, error) { - err := c.create(g) + err := c.create(nil, g) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (c *FederationVFS) Create(g *api.Federation) (*api.Federation, error) { } func (c *FederationVFS) Update(g *api.Federation) (*api.Federation, error) { - err := c.update(g) + err := c.update(nil, g) if err != nil { return nil, err } diff --git a/pkg/client/simple/vfsclientset/instancegroup.go b/pkg/client/simple/vfsclientset/instancegroup.go index 457383069a167..ed8b3a0bd1e1d 100644 --- a/pkg/client/simple/vfsclientset/instancegroup.go +++ b/pkg/client/simple/vfsclientset/instancegroup.go @@ -35,6 +35,7 @@ type InstanceGroupVFS struct { commonVFS clusterName string + cluster *kops.Cluster } type InstanceGroupMirror interface { @@ -43,10 +44,16 @@ type InstanceGroupMirror interface { var _ InstanceGroupMirror = &InstanceGroupVFS{} -func NewInstanceGroupMirror(clusterName string, configBase vfs.Path) InstanceGroupMirror { +func NewInstanceGroupMirror(cluster *kops.Cluster, configBase vfs.Path) InstanceGroupMirror { + if cluster == nil || cluster.Name == "" { + glog.Fatalf("cluster / cluster.Name is required") + } + + clusterName := cluster.Name kind := "InstanceGroup" r := &InstanceGroupVFS{ + cluster: cluster, clusterName: clusterName, } r.init(kind, configBase.Join("instancegroup"), StoreVersion) @@ -58,14 +65,16 @@ func NewInstanceGroupMirror(clusterName string, configBase vfs.Path) InstanceGro return r } -func newInstanceGroupVFS(c *VFSClientset, clusterName string) *InstanceGroupVFS { - if clusterName == "" { - glog.Fatalf("clusterName is required") +func newInstanceGroupVFS(c *VFSClientset, cluster *kops.Cluster) *InstanceGroupVFS { + if cluster == nil || cluster.Name == "" { + glog.Fatalf("cluster / cluster.Name is required") } + clusterName := cluster.Name kind := "InstanceGroup" r := &InstanceGroupVFS{ + cluster: cluster, clusterName: clusterName, } r.init(kind, c.basePath.Join(clusterName, "instancegroup"), StoreVersion) @@ -119,7 +128,7 @@ func (c *InstanceGroupVFS) List(options metav1.ListOptions) (*api.InstanceGroupL } func (c *InstanceGroupVFS) Create(g *api.InstanceGroup) (*api.InstanceGroup, error) { - err := c.create(g) + err := c.create(c.cluster, g) if err != nil { return nil, err } @@ -127,7 +136,7 @@ func (c *InstanceGroupVFS) Create(g *api.InstanceGroup) (*api.InstanceGroup, err } func (c *InstanceGroupVFS) Update(g *api.InstanceGroup) (*api.InstanceGroup, error) { - err := c.update(g) + err := c.update(c.cluster, g) if err != nil { return nil, err } @@ -135,7 +144,7 @@ func (c *InstanceGroupVFS) Update(g *api.InstanceGroup) (*api.InstanceGroup, err } func (c *InstanceGroupVFS) WriteMirror(g *api.InstanceGroup) error { - err := c.writeConfig(c.basePath.Join(g.Name), g) + err := c.writeConfig(c.cluster, c.basePath.Join(g.Name), g) if err != nil { return fmt.Errorf("error writing %s: %v", c.kind, err) } diff --git a/pkg/model/gcemodel/storageacl.go b/pkg/model/gcemodel/storageacl.go index 10935e8079969..d5ca546d365e2 100644 --- a/pkg/model/gcemodel/storageacl.go +++ b/pkg/model/gcemodel/storageacl.go @@ -50,12 +50,13 @@ func (b *StorageAclBuilder) Build(c *fi.ModelBuilderContext) error { case *vfs.GSPath: // It's not ideal that we have to do this at the bucket level, // but GCS doesn't seem to have a way to do subtrees (like AWS IAM does) - c.AddTask(&gcetasks.StorageBucketIam{ - Name: s("serviceaccount-statestore-read"), + // Note this permission only lets us list objects, not read them + c.AddTask(&gcetasks.StorageBucketAcl{ + Name: s("serviceaccount-statestore-list"), Lifecycle: b.Lifecycle, Bucket: s(p.Bucket()), - Entity: s("serviceAccount:" + serviceAccount), - Role: s("roles/storage.objectViewer"), + Entity: s("user-" + serviceAccount), + Role: s("READER"), }) } diff --git a/upup/models/vfs.go b/upup/models/vfs.go index aefb720344209..46397de036803 100644 --- a/upup/models/vfs.go +++ b/upup/models/vfs.go @@ -49,11 +49,11 @@ func (p *AssetPath) Join(relativePath ...string) vfs.Path { return &AssetPath{location: joined} } -func (p *AssetPath) WriteFile(data []byte) error { +func (p *AssetPath) WriteFile(data []byte, acl vfs.ACL) error { return ReadOnlyError } -func (p *AssetPath) CreateFile(data []byte) error { +func (p *AssetPath) CreateFile(data []byte, acl vfs.ACL) error { return ReadOnlyError } diff --git a/upup/pkg/fi/BUILD.bazel b/upup/pkg/fi/BUILD.bazel index a326a383681bb..7ce5d6f0dcb59 100644 --- a/upup/pkg/fi/BUILD.bazel +++ b/upup/pkg/fi/BUILD.bazel @@ -32,6 +32,7 @@ go_library( ], visibility = ["//visibility:public"], deps = [ + "//pkg/acls:go_default_library", "//pkg/apis/kops:go_default_library", "//pkg/assets:go_default_library", "//pkg/client/clientset_generated/clientset/typed/kops/internalversion:go_default_library", diff --git a/upup/pkg/fi/assettasks/BUILD.bazel b/upup/pkg/fi/assettasks/BUILD.bazel index 038357ee092dc..e1f3fd556d9c7 100644 --- a/upup/pkg/fi/assettasks/BUILD.bazel +++ b/upup/pkg/fi/assettasks/BUILD.bazel @@ -12,6 +12,7 @@ go_library( ], visibility = ["//visibility:public"], deps = [ + "//pkg/acls:go_default_library", "//upup/pkg/fi:go_default_library", "//util/pkg/hashing:go_default_library", "//util/pkg/vfs:go_default_library", diff --git a/upup/pkg/fi/assettasks/copyfile.go b/upup/pkg/fi/assettasks/copyfile.go index 01c57fc98c757..99506b9976ffc 100644 --- a/upup/pkg/fi/assettasks/copyfile.go +++ b/upup/pkg/fi/assettasks/copyfile.go @@ -17,17 +17,16 @@ limitations under the License. package assettasks import ( + "bytes" "fmt" - "net/url" - - "bytes" + "strings" "github.com/golang/glog" + "k8s.io/kops/pkg/acls" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/util/pkg/hashing" "k8s.io/kops/util/pkg/vfs" - "strings" ) // CopyFile copies an from a source file repository, to a target repository, @@ -83,14 +82,14 @@ func (_ *CopyFile) Render(c *fi.Context, a, e, changes *CopyFile) error { glog.Infof("copying bits from %q to %q", source, target) - if err := transferFile(source, target, sourceSha, sourceSHALocation); err != nil { + if err := transferFile(c, source, target, sourceSha, sourceSHALocation); err != nil { return fmt.Errorf("unable to transfer %q to %q: %v", source, target, err) } return nil } -func transferFile(source string, target string, sourceSHA string, sourceSHALocation string) error { +func transferFile(c *fi.Context, source string, target string, sourceSHA string, sourceSHALocation string) error { data, err := vfs.Context.ReadFile(source) if err != nil { return fmt.Errorf("Error unable to read path %q: %v", source, err) @@ -139,28 +138,33 @@ func transferFile(source string, target string, sourceSHA string, sourceSHALocat } b := bytes.NewBufferString(sha) - if err := writeFile(shaVFS, b.Bytes()); err != nil { + if err := writeFile(c, shaVFS, b.Bytes()); err != nil { return fmt.Errorf("Error uploading file %q: %v", shaVFS, err) } } } - if err := writeFile(uploadVFS, data); err != nil { + if err := writeFile(c, uploadVFS, data); err != nil { return fmt.Errorf("Error uploading file %q: %v", uploadVFS, err) } return nil } -func writeFile(vfsPath string, data []byte) error { +func writeFile(c *fi.Context, vfsPath string, data []byte) error { glog.V(2).Infof("uploading to %q", vfsPath) - destinationRegistry, err := vfs.Context.BuildVfsPath(vfsPath) + p, err := vfs.Context.BuildVfsPath(vfsPath) if err != nil { - return fmt.Errorf("Error parsing registry path %q: %v", vfsPath, err) + return fmt.Errorf("error building path %q: %v", vfsPath, err) + } + + acl, err := acls.GetACL(p, c.Cluster) + if err != nil { + return err } - if err = destinationRegistry.WriteFile(data); err != nil { - return fmt.Errorf("Error destination path %q: %v", vfsPath, err) + if err = p.WriteFile(data, acl); err != nil { + return fmt.Errorf("error writing path %q: %v", vfsPath, err) } glog.V(2).Infof("upload complete: %q", vfsPath) diff --git a/upup/pkg/fi/clientset_castore.go b/upup/pkg/fi/clientset_castore.go index eb807cc07ac9e..0344d21d2c9d4 100644 --- a/upup/pkg/fi/clientset_castore.go +++ b/upup/pkg/fi/clientset_castore.go @@ -31,6 +31,7 @@ import ( "golang.org/x/crypto/ssh" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kops/pkg/acls" "k8s.io/kops/pkg/apis/kops" kopsinternalversion "k8s.io/kops/pkg/client/clientset_generated/clientset/typed/kops/internalversion" "k8s.io/kops/pkg/pki" @@ -39,6 +40,7 @@ import ( // ClientsetCAStore is a CAStore implementation that stores keypairs in Keyset on a API server type ClientsetCAStore struct { + cluster *kops.Cluster namespace string clientset kopsinternalversion.KopsInterface @@ -49,8 +51,9 @@ type ClientsetCAStore struct { var _ CAStore = &ClientsetCAStore{} // NewClientsetCAStore is the constructor for ClientsetCAStore -func NewClientsetCAStore(clientset kopsinternalversion.KopsInterface, namespace string) CAStore { +func NewClientsetCAStore(cluster *kops.Cluster, clientset kopsinternalversion.KopsInterface, namespace string) CAStore { c := &ClientsetCAStore{ + cluster: cluster, clientset: clientset, namespace: namespace, cachedCaKeysets: make(map[string]*keyset), @@ -643,14 +646,24 @@ func (c *ClientsetCAStore) MirrorTo(basedir vfs.Path) error { item := &keyset.Spec.Keys[i] { p := basedir.Join("issued", keyset.Name, item.Id+".crt") - err = p.WriteFile(item.PublicMaterial) + acl, err := acls.GetACL(p, c.cluster) + if err != nil { + return err + } + + err = p.WriteFile(item.PublicMaterial, acl) if err != nil { return fmt.Errorf("error writing %q: %v", p, err) } } { p := basedir.Join("private", keyset.Name, item.Id+".key") - err = p.WriteFile(item.PrivateMaterial) + acl, err := acls.GetACL(p, c.cluster) + if err != nil { + return err + } + + err = p.WriteFile(item.PrivateMaterial, acl) if err != nil { return fmt.Errorf("error writing %q: %v", p, err) } @@ -684,7 +697,12 @@ func (c *ClientsetCAStore) MirrorTo(basedir vfs.Path) error { id := formatFingerprint(h.Sum(nil)) p := basedir.Join("ssh", "public", sshCredential.Name, id) - err = p.WriteFile([]byte(sshCredential.Spec.PublicKey)) + acl, err := acls.GetACL(p, c.cluster) + if err != nil { + return err + } + + err = p.WriteFile([]byte(sshCredential.Spec.PublicKey), acl) if err != nil { return fmt.Errorf("error writing %q: %v", p, err) } diff --git a/upup/pkg/fi/cloudup/apply_cluster.go b/upup/pkg/fi/cloudup/apply_cluster.go index 98e9c1248c63f..2ca7f99f83e49 100644 --- a/upup/pkg/fi/cloudup/apply_cluster.go +++ b/upup/pkg/fi/cloudup/apply_cluster.go @@ -22,6 +22,8 @@ import ( "strings" "time" + "github.com/blang/semver" + "github.com/golang/glog" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kopsbase "k8s.io/kops" "k8s.io/kops/pkg/apis/kops" @@ -31,6 +33,7 @@ import ( "k8s.io/kops/pkg/apis/nodeup" "k8s.io/kops/pkg/assets" "k8s.io/kops/pkg/client/simple" + "k8s.io/kops/pkg/client/simple/vfsclientset" "k8s.io/kops/pkg/dns" "k8s.io/kops/pkg/featureflag" "k8s.io/kops/pkg/model" @@ -45,6 +48,7 @@ import ( "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awstasks" "k8s.io/kops/upup/pkg/fi/cloudup/awsup" + "k8s.io/kops/upup/pkg/fi/cloudup/baremetal" "k8s.io/kops/upup/pkg/fi/cloudup/cloudformation" "k8s.io/kops/upup/pkg/fi/cloudup/do" "k8s.io/kops/upup/pkg/fi/cloudup/dotasks" @@ -56,11 +60,6 @@ import ( "k8s.io/kops/upup/pkg/fi/fitasks" "k8s.io/kops/util/pkg/hashing" "k8s.io/kops/util/pkg/vfs" - - "github.com/blang/semver" - "github.com/golang/glog" - "k8s.io/kops/pkg/client/simple/vfsclientset" - "k8s.io/kops/upup/pkg/fi/cloudup/baremetal" ) const ( @@ -808,12 +807,12 @@ func (c *ApplyClusterCmd) Run() error { c.Target = target if !dryRun { - err = registry.WriteConfigDeprecated(configBase.Join(registry.PathClusterCompleted), c.Cluster) + err = registry.WriteConfigDeprecated(cluster, configBase.Join(registry.PathClusterCompleted), c.Cluster) if err != nil { return fmt.Errorf("error writing completed cluster spec: %v", err) } - vfsMirror := vfsclientset.NewInstanceGroupMirror(cluster.Name, configBase) + vfsMirror := vfsclientset.NewInstanceGroupMirror(cluster, configBase) for _, g := range c.InstanceGroups { // TODO: We need to update the mirror (below), but do we need to update the primary? @@ -829,7 +828,7 @@ func (c *ApplyClusterCmd) Run() error { } } - context, err := fi.NewContext(target, cloud, keyStore, secretStore, configBase, checkExisting, taskMap) + context, err := fi.NewContext(target, cluster, cloud, keyStore, secretStore, configBase, checkExisting, taskMap) if err != nil { return fmt.Errorf("error building context: %v", err) } diff --git a/upup/pkg/fi/cloudup/awstasks/elastic_ip_test.go b/upup/pkg/fi/cloudup/awstasks/elastic_ip_test.go index 23b878fc80eea..d056bd64427e3 100644 --- a/upup/pkg/fi/cloudup/awstasks/elastic_ip_test.go +++ b/upup/pkg/fi/cloudup/awstasks/elastic_ip_test.go @@ -69,7 +69,7 @@ func TestElasticIPCreate(t *testing.T) { Cloud: cloud, } - context, err := fi.NewContext(target, cloud, nil, nil, nil, true, allTasks) + context, err := fi.NewContext(target, nil, cloud, nil, nil, nil, true, allTasks) if err != nil { t.Fatalf("error building context: %v", err) } @@ -106,7 +106,7 @@ func TestElasticIPCreate(t *testing.T) { func checkNoChanges(t *testing.T, cloud fi.Cloud, allTasks map[string]fi.Task) { assetBuilder := assets.NewAssetBuilder(nil) target := fi.NewDryRunTarget(assetBuilder, os.Stderr) - context, err := fi.NewContext(target, cloud, nil, nil, nil, true, allTasks) + context, err := fi.NewContext(target, nil, cloud, nil, nil, nil, true, allTasks) if err != nil { t.Fatalf("error building context: %v", err) } diff --git a/upup/pkg/fi/cloudup/awstasks/securitygroup_test.go b/upup/pkg/fi/cloudup/awstasks/securitygroup_test.go index e3e0e495c464e..09fe5d2439a36 100644 --- a/upup/pkg/fi/cloudup/awstasks/securitygroup_test.go +++ b/upup/pkg/fi/cloudup/awstasks/securitygroup_test.go @@ -125,7 +125,7 @@ func TestSecurityGroupCreate(t *testing.T) { Cloud: cloud, } - context, err := fi.NewContext(target, cloud, nil, nil, nil, true, allTasks) + context, err := fi.NewContext(target, nil, cloud, nil, nil, nil, true, allTasks) if err != nil { t.Fatalf("error building context: %v", err) } diff --git a/upup/pkg/fi/cloudup/awstasks/vpc_test.go b/upup/pkg/fi/cloudup/awstasks/vpc_test.go index 50b1c05e7b6b8..68c867900ea42 100644 --- a/upup/pkg/fi/cloudup/awstasks/vpc_test.go +++ b/upup/pkg/fi/cloudup/awstasks/vpc_test.go @@ -51,7 +51,7 @@ func TestVPCCreate(t *testing.T) { Cloud: cloud, } - context, err := fi.NewContext(target, cloud, nil, nil, nil, true, allTasks) + context, err := fi.NewContext(target, nil, cloud, nil, nil, nil, true, allTasks) if err != nil { t.Fatalf("error building context: %v", err) } diff --git a/upup/pkg/fi/cloudup/gcetasks/BUILD.bazel b/upup/pkg/fi/cloudup/gcetasks/BUILD.bazel index 345b519cc46b5..02390d435dbdc 100644 --- a/upup/pkg/fi/cloudup/gcetasks/BUILD.bazel +++ b/upup/pkg/fi/cloudup/gcetasks/BUILD.bazel @@ -20,8 +20,12 @@ go_library( "instancetemplate_fitask.go", "network.go", "network_fitask.go", + "storagebucketacl.go", + "storagebucketacl_fitask.go", "storagebucketiam.go", "storagebucketiam_fitask.go", + "storageobjectacl.go", + "storageobjectacl_fitask.go", "subnet.go", "subnet_fitask.go", "targetpool.go", diff --git a/upup/pkg/fi/cloudup/gcetasks/storagebucketacl.go b/upup/pkg/fi/cloudup/gcetasks/storagebucketacl.go new file mode 100644 index 0000000000000..1fbeefc626770 --- /dev/null +++ b/upup/pkg/fi/cloudup/gcetasks/storagebucketacl.go @@ -0,0 +1,133 @@ +/* +Copyright 2017 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 ( + "fmt" + + "github.com/golang/glog" + "google.golang.org/api/storage/v1" + "k8s.io/kops/upup/pkg/fi" + "k8s.io/kops/upup/pkg/fi/cloudup/gce" + "k8s.io/kops/upup/pkg/fi/cloudup/terraform" +) + +// StorageBucketAcl represents an ACL rule on a google cloud storage storage bucket +//go:generate fitask -type=StorageBucketAcl +type StorageBucketAcl struct { + Name *string + Lifecycle *fi.Lifecycle + + Bucket *string + Entity *string + + Role *string +} + +var _ fi.CompareWithID = &StorageBucketAcl{} + +func (e *StorageBucketAcl) CompareWithID() *string { + return e.Name +} + +func (e *StorageBucketAcl) Find(c *fi.Context) (*StorageBucketAcl, error) { + cloud := c.Cloud.(gce.GCECloud) + + bucket := fi.StringValue(e.Bucket) + entity := fi.StringValue(e.Entity) + + glog.V(2).Infof("Checking GCS Object ACL for gs://%s for %s", bucket, entity) + r, err := cloud.Storage().BucketAccessControls.Get(bucket, entity).Do() + if err != nil { + if gce.IsNotFound(err) { + return nil, nil + } + return nil, fmt.Errorf("error checking GCS Object ACL for gs://%s for %s: %v", bucket, entity, err) + } + + actual := &StorageBucketAcl{} + actual.Name = e.Name + actual.Bucket = &r.Bucket + actual.Entity = &r.Entity + + actual.Role = &r.Role + + // Ignore "system" fields + actual.Lifecycle = e.Lifecycle + + return actual, nil +} + +func (e *StorageBucketAcl) Run(c *fi.Context) error { + return fi.DefaultDeltaRunMethod(e, c) +} + +func (_ *StorageBucketAcl) CheckChanges(a, e, changes *StorageBucketAcl) error { + if fi.StringValue(e.Bucket) == "" { + return fi.RequiredField("Bucket") + } + if fi.StringValue(e.Entity) == "" { + return fi.RequiredField("Entity") + } + return nil +} + +func (_ *StorageBucketAcl) RenderGCE(t *gce.GCEAPITarget, a, e, changes *StorageBucketAcl) error { + bucket := fi.StringValue(e.Bucket) + entity := fi.StringValue(e.Entity) + role := fi.StringValue(e.Role) + + acl := &storage.BucketAccessControl{ + Entity: entity, + Role: role, + } + + if a == nil { + glog.V(2).Infof("Creating GCS bucket ACL for gs://%s for %s as %s", bucket, entity, role) + + _, err := t.Cloud.Storage().BucketAccessControls.Insert(bucket, acl).Do() + if err != nil { + return fmt.Errorf("error creating GCS bucket ACL for gs://%s for %s as %s: %v", bucket, entity, role, err) + } + } else { + glog.V(2).Infof("Updating GCS Object ACL for gs://%s for %s as %s", bucket, entity, role) + + _, err := t.Cloud.Storage().BucketAccessControls.Update(bucket, entity, acl).Do() + if err != nil { + return fmt.Errorf("error updating GCS bucket ACL for gs://%s for %s as %s: %v", bucket, entity, role, err) + } + } + + return nil +} + +// terraformStorageBucketAcl is the model for a terraform google_storage_bucket_acl rule +type terraformStorageBucketAcl struct { + Bucket string `json:"bucket,omitempty"` + RoleEntity []string `json:"role_entity,omitempty"` +} + +func (_ *StorageBucketAcl) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *StorageBucketAcl) error { + var roleEntities []string + roleEntities = append(roleEntities, fi.StringValue(e.Role)+":"+fi.StringValue(e.Name)) + tf := &terraformStorageBucketAcl{ + Bucket: fi.StringValue(e.Bucket), + RoleEntity: roleEntities, + } + + return t.RenderResource("google_storage_bucket_acl", *e.Name, tf) +} diff --git a/upup/pkg/fi/cloudup/gcetasks/storagebucketacl_fitask.go b/upup/pkg/fi/cloudup/gcetasks/storagebucketacl_fitask.go new file mode 100644 index 0000000000000..16242116f51b6 --- /dev/null +++ b/upup/pkg/fi/cloudup/gcetasks/storagebucketacl_fitask.go @@ -0,0 +1,70 @@ +/* +Copyright 2017 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" -type=StorageBucketAcl"; DO NOT EDIT + +package gcetasks + +import ( + "encoding/json" + + "k8s.io/kops/upup/pkg/fi" +) + +// StorageBucketAcl + +// JSON marshalling boilerplate +type realStorageBucketAcl StorageBucketAcl + +// UnmarshalJSON implements conversion to JSON, supporitng an alternate specification of the object as a string +func (o *StorageBucketAcl) UnmarshalJSON(data []byte) error { + var jsonName string + if err := json.Unmarshal(data, &jsonName); err == nil { + o.Name = &jsonName + return nil + } + + var r realStorageBucketAcl + if err := json.Unmarshal(data, &r); err != nil { + return err + } + *o = StorageBucketAcl(r) + return nil +} + +var _ fi.HasLifecycle = &StorageBucketAcl{} + +// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle +func (o *StorageBucketAcl) GetLifecycle() *fi.Lifecycle { + return o.Lifecycle +} + +var _ fi.HasName = &StorageBucketAcl{} + +// GetName returns the Name of the object, implementing fi.HasName +func (o *StorageBucketAcl) GetName() *string { + return o.Name +} + +// SetName sets the Name of the object, implementing fi.SetName +func (o *StorageBucketAcl) SetName(name string) { + o.Name = &name +} + +// String is the stringer function for the task, producing readable output using fi.TaskAsString +func (o *StorageBucketAcl) String() string { + return fi.TaskAsString(o) +} diff --git a/upup/pkg/fi/cloudup/gcetasks/storageobjectacl.go b/upup/pkg/fi/cloudup/gcetasks/storageobjectacl.go new file mode 100644 index 0000000000000..e919a3d745abf --- /dev/null +++ b/upup/pkg/fi/cloudup/gcetasks/storageobjectacl.go @@ -0,0 +1,142 @@ +/* +Copyright 2017 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 ( + "fmt" + + "github.com/golang/glog" + "google.golang.org/api/storage/v1" + "k8s.io/kops/upup/pkg/fi" + "k8s.io/kops/upup/pkg/fi/cloudup/gce" + "k8s.io/kops/upup/pkg/fi/cloudup/terraform" +) + +// StorageObjectAcl represents an ACL rule on a google cloud storage storage object +//go:generate fitask -type=StorageObjectAcl +type StorageObjectAcl struct { + Name *string + Lifecycle *fi.Lifecycle + + Bucket *string + Object *string + Entity *string + + Role *string +} + +var _ fi.CompareWithID = &StorageObjectAcl{} + +func (e *StorageObjectAcl) CompareWithID() *string { + return e.Name +} + +func (e *StorageObjectAcl) Find(c *fi.Context) (*StorageObjectAcl, error) { + cloud := c.Cloud.(gce.GCECloud) + + bucket := fi.StringValue(e.Bucket) + object := fi.StringValue(e.Object) + entity := fi.StringValue(e.Entity) + + glog.V(2).Infof("Checking GCS Object ACL for gs://%s/%s for %s", bucket, object, entity) + r, err := cloud.Storage().ObjectAccessControls.Get(bucket, object, entity).Do() + if err != nil { + if gce.IsNotFound(err) { + return nil, nil + } + return nil, fmt.Errorf("error querying GCS Object ACL for gs://%s/%s for %s: %v", bucket, object, entity, err) + } + + actual := &StorageObjectAcl{} + actual.Name = e.Name + actual.Bucket = &r.Bucket + actual.Object = &r.Object + actual.Entity = &r.Entity + + actual.Role = &r.Role + + // Ignore "system" fields + actual.Lifecycle = e.Lifecycle + + return actual, nil +} + +func (e *StorageObjectAcl) Run(c *fi.Context) error { + return fi.DefaultDeltaRunMethod(e, c) +} + +func (_ *StorageObjectAcl) CheckChanges(a, e, changes *StorageObjectAcl) error { + if fi.StringValue(e.Bucket) == "" { + return fi.RequiredField("Bucket") + } + if fi.StringValue(e.Object) == "" { + return fi.RequiredField("Object") + } + if fi.StringValue(e.Entity) == "" { + return fi.RequiredField("Entity") + } + return nil +} + +func (_ *StorageObjectAcl) RenderGCE(t *gce.GCEAPITarget, a, e, changes *StorageObjectAcl) error { + bucket := fi.StringValue(e.Bucket) + object := fi.StringValue(e.Object) + entity := fi.StringValue(e.Entity) + role := fi.StringValue(e.Role) + + acl := &storage.ObjectAccessControl{ + Entity: entity, + Role: role, + } + + if a == nil { + glog.V(2).Infof("Creating GCS Object ACL for gs://%s/%s for %s as %s", bucket, object, entity, role) + + _, err := t.Cloud.Storage().ObjectAccessControls.Insert(bucket, object, acl).Do() + if err != nil { + return fmt.Errorf("error creating GCS Object ACL for gs://%s/%s for %s as %s: %v", bucket, object, entity, role, err) + } + } else { + glog.V(2).Infof("Updating GCS Object ACL for gs://%s/%s for %s as %s", bucket, object, entity, role) + + _, err := t.Cloud.Storage().ObjectAccessControls.Update(bucket, object, entity, acl).Do() + if err != nil { + return fmt.Errorf("error updating GCS Object ACL for gs://%s/%s for %s as %s: %v", bucket, object, entity, role, err) + } + } + + return nil +} + +// terraformStorageObjectAcl is the model for a terraform google_storage_object_acl rule +type terraformStorageObjectAcl struct { + Bucket string `json:"bucket,omitempty"` + Object string `json:"object,omitempty"` + RoleEntity []string `json:"role_entity,omitempty"` +} + +func (_ *StorageObjectAcl) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *StorageObjectAcl) error { + var roleEntities []string + roleEntities = append(roleEntities, fi.StringValue(e.Role)+":"+fi.StringValue(e.Name)) + tf := &terraformStorageObjectAcl{ + Bucket: fi.StringValue(e.Bucket), + Object: fi.StringValue(e.Object), + RoleEntity: roleEntities, + } + + return t.RenderResource("google_storage_object_acl", *e.Name, tf) +} diff --git a/upup/pkg/fi/cloudup/gcetasks/storageobjectacl_fitask.go b/upup/pkg/fi/cloudup/gcetasks/storageobjectacl_fitask.go new file mode 100644 index 0000000000000..ea2b0d2ac13c8 --- /dev/null +++ b/upup/pkg/fi/cloudup/gcetasks/storageobjectacl_fitask.go @@ -0,0 +1,70 @@ +/* +Copyright 2017 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" -type=StorageObjectAcl"; DO NOT EDIT + +package gcetasks + +import ( + "encoding/json" + + "k8s.io/kops/upup/pkg/fi" +) + +// StorageObjectAcl + +// JSON marshalling boilerplate +type realStorageObjectAcl StorageObjectAcl + +// UnmarshalJSON implements conversion to JSON, supporitng an alternate specification of the object as a string +func (o *StorageObjectAcl) UnmarshalJSON(data []byte) error { + var jsonName string + if err := json.Unmarshal(data, &jsonName); err == nil { + o.Name = &jsonName + return nil + } + + var r realStorageObjectAcl + if err := json.Unmarshal(data, &r); err != nil { + return err + } + *o = StorageObjectAcl(r) + return nil +} + +var _ fi.HasLifecycle = &StorageObjectAcl{} + +// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle +func (o *StorageObjectAcl) GetLifecycle() *fi.Lifecycle { + return o.Lifecycle +} + +var _ fi.HasName = &StorageObjectAcl{} + +// GetName returns the Name of the object, implementing fi.HasName +func (o *StorageObjectAcl) GetName() *string { + return o.Name +} + +// SetName sets the Name of the object, implementing fi.SetName +func (o *StorageObjectAcl) SetName(name string) { + o.Name = &name +} + +// String is the stringer function for the task, producing readable output using fi.TaskAsString +func (o *StorageObjectAcl) String() string { + return fi.TaskAsString(o) +} diff --git a/upup/pkg/fi/context.go b/upup/pkg/fi/context.go index cc9acd4d01a91..6da0e6ba91f9b 100644 --- a/upup/pkg/fi/context.go +++ b/upup/pkg/fi/context.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/golang/glog" "io/ioutil" + "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/util/pkg/vfs" "k8s.io/kubernetes/federation/pkg/dnsprovider" "os" @@ -35,6 +36,7 @@ type Context struct { Target Target DNS dnsprovider.Interface Cloud Cloud + Cluster *kops.Cluster Keystore Keystore SecretStore SecretStore ClusterConfigBase vfs.Path @@ -44,9 +46,10 @@ type Context struct { tasks map[string]Task } -func NewContext(target Target, cloud Cloud, keystore Keystore, secretStore SecretStore, clusterConfigBase vfs.Path, checkExisting bool, tasks map[string]Task) (*Context, error) { +func NewContext(target Target, cluster *kops.Cluster, cloud Cloud, keystore Keystore, secretStore SecretStore, clusterConfigBase vfs.Path, checkExisting bool, tasks map[string]Task) (*Context, error) { c := &Context{ Cloud: cloud, + Cluster: cluster, Target: target, Keystore: keystore, SecretStore: secretStore, diff --git a/upup/pkg/fi/fitasks/BUILD.bazel b/upup/pkg/fi/fitasks/BUILD.bazel index 861f0779438cc..12384ede62bca 100644 --- a/upup/pkg/fi/fitasks/BUILD.bazel +++ b/upup/pkg/fi/fitasks/BUILD.bazel @@ -17,6 +17,7 @@ go_library( ], visibility = ["//visibility:public"], deps = [ + "//pkg/acls:go_default_library", "//pkg/pki:go_default_library", "//upup/pkg/fi:go_default_library", "//upup/pkg/fi/secrets:go_default_library", diff --git a/upup/pkg/fi/fitasks/managedfile.go b/upup/pkg/fi/fitasks/managedfile.go index dd45e54d085ec..26f3d20e10a2b 100644 --- a/upup/pkg/fi/fitasks/managedfile.go +++ b/upup/pkg/fi/fitasks/managedfile.go @@ -19,6 +19,7 @@ package fitasks import ( "fmt" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/kops/pkg/acls" "k8s.io/kops/upup/pkg/fi" "os" ) @@ -87,7 +88,14 @@ func (_ *ManagedFile) Render(c *fi.Context, a, e, changes *ManagedFile) error { return fmt.Errorf("error reading contents of ManagedFile: %v", err) } - err = c.ClusterConfigBase.Join(location).WriteFile(data) + p := c.ClusterConfigBase.Join(location) + + acl, err := acls.GetACL(p, c.Cluster) + if err != nil { + return err + } + + err = p.WriteFile(data, acl) if err != nil { return fmt.Errorf("error creating ManagedFile %q: %v", location, err) } diff --git a/upup/pkg/fi/fitasks/mirrorsecrets.go b/upup/pkg/fi/fitasks/mirrorsecrets.go index da70bec6c09f8..407788a06b61e 100644 --- a/upup/pkg/fi/fitasks/mirrorsecrets.go +++ b/upup/pkg/fi/fitasks/mirrorsecrets.go @@ -75,6 +75,5 @@ func (s *MirrorSecrets) CheckChanges(a, e, changes *MirrorSecrets) error { // Render implements fi.Task::Render func (_ *MirrorSecrets) Render(c *fi.Context, a, e, changes *MirrorSecrets) error { secrets := c.SecretStore - return secrets.MirrorTo(e.MirrorPath) } diff --git a/upup/pkg/fi/nodeup/command.go b/upup/pkg/fi/nodeup/command.go index fa58f5d9c27fe..50a00fe9f7d67 100644 --- a/upup/pkg/fi/nodeup/command.go +++ b/upup/pkg/fi/nodeup/command.go @@ -258,7 +258,7 @@ func (c *NodeUpCommand) Run(out io.Writer) error { return fmt.Errorf("unsupported target type %q", c.Target) } - context, err := fi.NewContext(target, cloud, keyStore, secretStore, configBase, checkExisting, taskMap) + context, err := fi.NewContext(target, nil, cloud, keyStore, secretStore, configBase, checkExisting, taskMap) if err != nil { glog.Exitf("error building context: %v", err) } diff --git a/upup/pkg/fi/nodeup/template_functions.go b/upup/pkg/fi/nodeup/template_functions.go index df77a0f497bf4..ef711563eb076 100644 --- a/upup/pkg/fi/nodeup/template_functions.go +++ b/upup/pkg/fi/nodeup/template_functions.go @@ -67,7 +67,7 @@ func newTemplateFunctions(nodeupConfig *nodeup.Config, cluster *api.Cluster, ins return nil, fmt.Errorf("error building secret store path: %v", err) } - t.secretStore = secrets.NewVFSSecretStore(p) + t.secretStore = secrets.NewVFSSecretStore(cluster, p) } else { return nil, fmt.Errorf("SecretStore not set") } @@ -79,7 +79,7 @@ func newTemplateFunctions(nodeupConfig *nodeup.Config, cluster *api.Cluster, ins return nil, fmt.Errorf("error building key store path: %v", err) } - t.keyStore = fi.NewVFSCAStore(p) + t.keyStore = fi.NewVFSCAStore(cluster, p) } else { return nil, fmt.Errorf("KeyStore not set") } diff --git a/upup/pkg/fi/secrets/BUILD.bazel b/upup/pkg/fi/secrets/BUILD.bazel index 3e6e227048807..f6611314f9ac4 100644 --- a/upup/pkg/fi/secrets/BUILD.bazel +++ b/upup/pkg/fi/secrets/BUILD.bazel @@ -8,6 +8,7 @@ go_library( ], visibility = ["//visibility:public"], deps = [ + "//pkg/acls:go_default_library", "//pkg/apis/kops:go_default_library", "//pkg/client/clientset_generated/clientset/typed/kops/internalversion:go_default_library", "//pkg/pki:go_default_library", diff --git a/upup/pkg/fi/secrets/clientset_secretstore.go b/upup/pkg/fi/secrets/clientset_secretstore.go index c5ae07b63da2e..5ea69eabb67d2 100644 --- a/upup/pkg/fi/secrets/clientset_secretstore.go +++ b/upup/pkg/fi/secrets/clientset_secretstore.go @@ -25,6 +25,7 @@ import ( "github.com/golang/glog" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kops/pkg/acls" "k8s.io/kops/pkg/apis/kops" kopsinternalversion "k8s.io/kops/pkg/client/clientset_generated/clientset/typed/kops/internalversion" "k8s.io/kops/pkg/pki" @@ -37,6 +38,7 @@ const NamePrefix = "token-" // ClientsetSecretStore is a SecretStore backed by Keyset objects in an API server type ClientsetSecretStore struct { + cluster *kops.Cluster namespace string clientset kopsinternalversion.KopsInterface } @@ -44,8 +46,9 @@ type ClientsetSecretStore struct { var _ fi.SecretStore = &ClientsetSecretStore{} // NewClientsetSecretStore is the constructor for ClientsetSecretStore -func NewClientsetSecretStore(clientset kopsinternalversion.KopsInterface, namespace string) fi.SecretStore { +func NewClientsetSecretStore(cluster *kops.Cluster, clientset kopsinternalversion.KopsInterface, namespace string) fi.SecretStore { c := &ClientsetSecretStore{ + cluster: cluster, clientset: clientset, namespace: namespace, } @@ -81,7 +84,12 @@ func (c *ClientsetSecretStore) MirrorTo(basedir vfs.Path) error { return fmt.Errorf("error serializing secret: %v", err) } - if err := p.WriteFile(data); err != nil { + acl, err := acls.GetACL(p, c.cluster) + if err != nil { + return err + } + + if err := p.WriteFile(data, acl); err != nil { return fmt.Errorf("error writing secret to %q: %v", p, err) } } diff --git a/upup/pkg/fi/secrets/vfs_secretstore.go b/upup/pkg/fi/secrets/vfs_secretstore.go index a7eb32e5df91b..14e3ab6dcf60a 100644 --- a/upup/pkg/fi/secrets/vfs_secretstore.go +++ b/upup/pkg/fi/secrets/vfs_secretstore.go @@ -20,19 +20,23 @@ import ( "encoding/json" "fmt" "github.com/golang/glog" + "k8s.io/kops/pkg/acls" + "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/util/pkg/vfs" "os" ) type VFSSecretStore struct { + cluster *kops.Cluster basedir vfs.Path } var _ fi.SecretStore = &VFSSecretStore{} -func NewVFSSecretStore(basedir vfs.Path) fi.SecretStore { +func NewVFSSecretStore(cluster *kops.Cluster, basedir vfs.Path) fi.SecretStore { c := &VFSSecretStore{ + cluster: cluster, basedir: basedir, } return c @@ -48,7 +52,7 @@ func (c *VFSSecretStore) MirrorTo(basedir vfs.Path) error { } glog.V(2).Infof("Mirroring secret store from %q to %q", c.basedir, basedir) - return vfs.CopyTree(c.basedir, basedir) + return vfs.CopyTree(c.basedir, basedir, func(p vfs.Path) (vfs.ACL, error) { return acls.GetACL(p, c.cluster) }) } func BuildVfsSecretPath(basedir vfs.Path, name string) vfs.Path { @@ -117,7 +121,12 @@ func (c *VFSSecretStore) GetOrCreateSecret(id string, secret *fi.Secret) (*fi.Se return s, false, nil } - err = c.createSecret(secret, p) + acl, err := acls.GetACL(p, c.cluster) + if err != nil { + return nil, false, err + } + + err = c.createSecret(secret, p, acl) if err != nil { if os.IsExist(err) && i == 0 { glog.Infof("Got already-exists error when writing secret; likely due to concurrent creation. Will retry") @@ -157,10 +166,10 @@ func (c *VFSSecretStore) loadSecret(p vfs.Path) (*fi.Secret, error) { } // createSecret writes the secret, but only if it does not exists -func (c *VFSSecretStore) createSecret(s *fi.Secret, p vfs.Path) error { +func (c *VFSSecretStore) createSecret(s *fi.Secret, p vfs.Path, acl vfs.ACL) error { data, err := json.Marshal(s) if err != nil { return fmt.Errorf("error serializing secret: %v", err) } - return p.CreateFile(data) + return p.CreateFile(data, acl) } diff --git a/upup/pkg/fi/vfs_castore.go b/upup/pkg/fi/vfs_castore.go index fb55be015d0d3..df3fb6413e13d 100644 --- a/upup/pkg/fi/vfs_castore.go +++ b/upup/pkg/fi/vfs_castore.go @@ -33,12 +33,15 @@ import ( "github.com/golang/glog" "golang.org/x/crypto/ssh" + "k8s.io/kops/pkg/acls" + "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/pki" "k8s.io/kops/util/pkg/vfs" ) type VFSCAStore struct { basedir vfs.Path + cluster *kops.Cluster mutex sync.Mutex cachedCAs map[string]*cachedEntry @@ -51,9 +54,10 @@ type cachedEntry struct { var _ CAStore = &VFSCAStore{} -func NewVFSCAStore(basedir vfs.Path) CAStore { +func NewVFSCAStore(cluster *kops.Cluster, basedir vfs.Path) CAStore { c := &VFSCAStore{ basedir: basedir, + cluster: cluster, cachedCAs: make(map[string]*cachedEntry), } @@ -417,7 +421,10 @@ func (c *VFSCAStore) MirrorTo(basedir vfs.Path) error { } glog.V(2).Infof("Mirroring key store from %q to %q", c.basedir, basedir) - return vfs.CopyTree(c.basedir, basedir) + aclOracle := func(p vfs.Path) (vfs.ACL, error) { + return acls.GetACL(p, c.cluster) + } + return vfs.CopyTree(c.basedir, basedir, aclOracle) } func (c *VFSCAStore) IssueCert(signer string, id string, serial *big.Int, privateKey *pki.PrivateKey, template *x509.Certificate) (*pki.Certificate, error) { @@ -625,7 +632,11 @@ func (c *VFSCAStore) storePrivateKey(privateKey *pki.PrivateKey, p vfs.Path) err return err } - return p.WriteFile(data.Bytes()) + acl, err := acls.GetACL(p, c.cluster) + if err != nil { + return err + } + return p.WriteFile(data.Bytes(), acl) } func (c *VFSCAStore) storeCertificate(cert *pki.Certificate, p vfs.Path) error { @@ -636,7 +647,11 @@ func (c *VFSCAStore) storeCertificate(cert *pki.Certificate, p vfs.Path) error { return err } - return p.WriteFile(data.Bytes()) + acl, err := acls.GetACL(p, c.cluster) + if err != nil { + return err + } + return p.WriteFile(data.Bytes(), acl) } func (c *VFSCAStore) buildSerial() *big.Int { @@ -699,7 +714,13 @@ func (c *VFSCAStore) AddSSHPublicKey(name string, pubkey []byte) error { } p := c.buildSSHPublicKeyPath(name, id) - return c.storeData(pubkey, p) + + acl, err := acls.GetACL(p, c.cluster) + if err != nil { + return err + } + + return p.WriteFile(pubkey, acl) } func (c *VFSCAStore) buildSSHPublicKeyPath(name string, id string) vfs.Path { @@ -708,10 +729,6 @@ func (c *VFSCAStore) buildSSHPublicKeyPath(name string, id string) vfs.Path { return c.basedir.Join("ssh", "public", name, id) } -func (c *VFSCAStore) storeData(data []byte, p vfs.Path) error { - return p.WriteFile(data) -} - func (c *VFSCAStore) FindSSHPublicKeys(name string) ([]*KeystoreItem, error) { p := c.basedir.Join("ssh", "public", name) diff --git a/upup/pkg/kutil/convert_kubeup_cluster.go b/upup/pkg/kutil/convert_kubeup_cluster.go index 12cdab8b074e5..e16c7cd50ad4a 100644 --- a/upup/pkg/kutil/convert_kubeup_cluster.go +++ b/upup/pkg/kutil/convert_kubeup_cluster.go @@ -471,7 +471,7 @@ func (x *ConvertKubeupCluster) Upgrade() error { } // TODO: No longer needed? - err = registry.WriteConfigDeprecated(newConfigBase.Join(registry.PathClusterCompleted), fullCluster) + err = registry.WriteConfigDeprecated(cluster, newConfigBase.Join(registry.PathClusterCompleted), fullCluster) if err != nil { return fmt.Errorf("error writing completed cluster spec: %v", err) } diff --git a/util/pkg/vfs/context.go b/util/pkg/vfs/context.go index 28307a97e2a94..f0b602552400e 100644 --- a/util/pkg/vfs/context.go +++ b/util/pkg/vfs/context.go @@ -284,6 +284,7 @@ func (c *VFSContext) buildGCSPath(p string) (*GSPath, error) { return gcsPath, nil } +// getGCSClient returns the google storage.Service client, caching it for future calls func (c *VFSContext) getGCSClient() (*storage.Service, error) { c.mutex.Lock() defer c.mutex.Unlock() diff --git a/util/pkg/vfs/fs.go b/util/pkg/vfs/fs.go index 960fb2b3458d0..ce71650734f8e 100644 --- a/util/pkg/vfs/fs.go +++ b/util/pkg/vfs/fs.go @@ -45,7 +45,7 @@ func (p *FSPath) Join(relativePath ...string) Path { return &FSPath{location: joined} } -func (p *FSPath) WriteFile(data []byte) error { +func (p *FSPath) WriteFile(data []byte, acl ACL) error { dir := path.Dir(p.location) err := os.MkdirAll(dir, 0755) if err != nil { @@ -94,7 +94,7 @@ func (p *FSPath) WriteFile(data []byte) error { // TODO: should we take a file lock or equivalent here? Can we use RENAME_NOREPLACE ? var createFileLock sync.Mutex -func (p *FSPath) CreateFile(data []byte) error { +func (p *FSPath) CreateFile(data []byte, acl ACL) error { createFileLock.Lock() defer createFileLock.Unlock() @@ -108,7 +108,7 @@ func (p *FSPath) CreateFile(data []byte) error { return err } - return p.WriteFile(data) + return p.WriteFile(data, acl) } func (p *FSPath) ReadFile() ([]byte, error) { diff --git a/util/pkg/vfs/gsfs.go b/util/pkg/vfs/gsfs.go index a2743e27b62a7..8ce42d71ffd64 100644 --- a/util/pkg/vfs/gsfs.go +++ b/util/pkg/vfs/gsfs.go @@ -55,6 +55,13 @@ var gcsReadBackoff = wait.Backoff{ Steps: 4, } +// GSAcl is an ACL implementation for objects on Google Cloud Storage +type GSAcl struct { + Acl []*storage.ObjectAccessControl +} + +var _ ACL = &GSAcl{} + // gcsWriteBackoff is the backoff strategy for GCS write retries var gcsWriteBackoff = wait.Backoff{ Duration: time.Second, @@ -86,6 +93,11 @@ func (p *GSPath) Object() string { return p.key } +// Client returns the storage.Service bound to this path +func (p *GSPath) Client() *storage.Service { + return p.client +} + func (p *GSPath) String() string { return p.Path() } @@ -122,7 +134,7 @@ func (p *GSPath) Join(relativePath ...string) Path { } } -func (p *GSPath) WriteFile(data []byte) error { +func (p *GSPath) WriteFile(data []byte, acl ACL) error { done, err := RetryWithBackoff(gcsWriteBackoff, func() (bool, error) { glog.V(4).Infof("Writing file %q", p) @@ -135,6 +147,15 @@ func (p *GSPath) WriteFile(data []byte) error { Name: p.key, Md5Hash: base64.StdEncoding.EncodeToString(md5Hash.HashValue), } + + if acl != nil { + gsAcl, ok := acl.(*GSAcl) + if !ok { + return true, fmt.Errorf("write to %s with ACL of unexpected type %T", p, acl) + } + obj.Acl = gsAcl.Acl + } + r := bytes.NewReader(data) _, err = p.client.Objects.Insert(p.bucket, obj).Media(r).Do() if err != nil { @@ -159,7 +180,7 @@ func (p *GSPath) WriteFile(data []byte) error { // TODO: should we enable versioning? var createFileLockGCS sync.Mutex -func (p *GSPath) CreateFile(data []byte) error { +func (p *GSPath) CreateFile(data []byte, acl ACL) error { createFileLockGCS.Lock() defer createFileLockGCS.Unlock() @@ -173,7 +194,7 @@ func (p *GSPath) CreateFile(data []byte) error { return err } - return p.WriteFile(data) + return p.WriteFile(data, acl) } // ReadFile implements Path::ReadFile diff --git a/util/pkg/vfs/k8sfs.go b/util/pkg/vfs/k8sfs.go index 9ce79e85a44bc..30637fea79e3e 100644 --- a/util/pkg/vfs/k8sfs.go +++ b/util/pkg/vfs/k8sfs.go @@ -76,11 +76,11 @@ func (p *KubernetesPath) Join(relativePath ...string) Path { } } -func (p *KubernetesPath) WriteFile(data []byte) error { +func (p *KubernetesPath) WriteFile(data []byte, acl ACL) error { return fmt.Errorf("KubernetesPath::WriteFile not supported") } -func (p *KubernetesPath) CreateFile(data []byte) error { +func (p *KubernetesPath) CreateFile(data []byte, acl ACL) error { return fmt.Errorf("KubernetesPath::CreateFile not supported") } diff --git a/util/pkg/vfs/memfs.go b/util/pkg/vfs/memfs.go index c103f9b942016..c297910b9c098 100644 --- a/util/pkg/vfs/memfs.go +++ b/util/pkg/vfs/memfs.go @@ -87,18 +87,18 @@ func (p *MemFSPath) Join(relativePath ...string) Path { return current } -func (p *MemFSPath) WriteFile(data []byte) error { +func (p *MemFSPath) WriteFile(data []byte, acl ACL) error { p.contents = data return nil } -func (p *MemFSPath) CreateFile(data []byte) error { +func (p *MemFSPath) CreateFile(data []byte, acl ACL) error { // Check if exists if p.contents != nil { return os.ErrExist } - return p.WriteFile(data) + return p.WriteFile(data, acl) } func (p *MemFSPath) ReadFile() ([]byte, error) { diff --git a/util/pkg/vfs/s3fs.go b/util/pkg/vfs/s3fs.go index 6a6c7a80f1b92..96966743406b3 100644 --- a/util/pkg/vfs/s3fs.go +++ b/util/pkg/vfs/s3fs.go @@ -103,7 +103,7 @@ func (p *S3Path) Join(relativePath ...string) Path { } } -func (p *S3Path) WriteFile(data []byte) error { +func (p *S3Path) WriteFile(data []byte, aclObj ACL) error { client, err := p.client() if err != nil { return err @@ -149,7 +149,7 @@ func (p *S3Path) WriteFile(data []byte) error { // TODO: should we enable versioning? var createFileLockS3 sync.Mutex -func (p *S3Path) CreateFile(data []byte) error { +func (p *S3Path) CreateFile(data []byte, acl ACL) error { createFileLockS3.Lock() defer createFileLockS3.Unlock() @@ -163,7 +163,7 @@ func (p *S3Path) CreateFile(data []byte) error { return err } - return p.WriteFile(data) + return p.WriteFile(data, acl) } func (p *S3Path) ReadFile() ([]byte, error) { diff --git a/util/pkg/vfs/sshfs.go b/util/pkg/vfs/sshfs.go index bae8c14d2ceb2..a0b0bf21e5cbe 100644 --- a/util/pkg/vfs/sshfs.go +++ b/util/pkg/vfs/sshfs.go @@ -142,7 +142,7 @@ func mkdirAll(sftpClient *sftp.Client, dir string) error { return nil } -func (p *SSHPath) WriteFile(data []byte) error { +func (p *SSHPath) WriteFile(data []byte, acl ACL) error { sftpClient, err := p.newClient() if err != nil { return err @@ -197,7 +197,7 @@ func (p *SSHPath) WriteFile(data []byte) error { // Not a great approach, but fine for a single process (with low concurrency) var createFileLockSSH sync.Mutex -func (p *SSHPath) CreateFile(data []byte) error { +func (p *SSHPath) CreateFile(data []byte, acl ACL) error { createFileLockSSH.Lock() defer createFileLockSSH.Unlock() @@ -211,7 +211,7 @@ func (p *SSHPath) CreateFile(data []byte) error { return err } - return p.WriteFile(data) + return p.WriteFile(data, acl) } func (p *SSHPath) ReadFile() ([]byte, error) { diff --git a/util/pkg/vfs/vfs.go b/util/pkg/vfs/vfs.go index 8be2fbc8151d7..c402ac1af93d3 100644 --- a/util/pkg/vfs/vfs.go +++ b/util/pkg/vfs/vfs.go @@ -34,13 +34,19 @@ func IsDirectory(p Path) bool { return err == nil } +type ACL interface { +} + +type ACLOracle func(Path) (ACL, error) + +// Path is a path in the VFS space, which we can read, write, list etc type Path interface { Join(relativePath ...string) Path ReadFile() ([]byte, error) - WriteFile(data []byte) error + WriteFile(data []byte, acl ACL) error // CreateFile writes the file contents, but only if the file does not already exist - CreateFile(data []byte) error + CreateFile(data []byte, acl ACL) error // Remove deletes the file Remove() error diff --git a/util/pkg/vfs/vfssync.go b/util/pkg/vfs/vfssync.go index 995c266dd69cd..51b28f9b560b8 100644 --- a/util/pkg/vfs/vfssync.go +++ b/util/pkg/vfs/vfssync.go @@ -161,7 +161,7 @@ func SyncDir(src *VFSScan, destBase Path) error { if destData == nil || !bytes.Equal(srcData, destData) { glog.V(2).Infof("Copying data from %s to %s", f, destFile) - err = destFile.WriteFile(srcData) + err = destFile.WriteFile(srcData, nil) if err != nil { return fmt.Errorf("error writing dest file %q: %v", f, err) } @@ -215,7 +215,7 @@ func hashesMatch(src, dest Path) (bool, error) { } // CopyTree copies all files in src to dest. It copies the whole recursive subtree of files. -func CopyTree(src Path, dest Path) error { +func CopyTree(src Path, dest Path, aclOracle ACLOracle) error { srcFiles, err := src.ReadTree() if err != nil { return fmt.Errorf("error reading source directory %q: %v", src, err) @@ -269,8 +269,13 @@ func CopyTree(src Path, dest Path) error { } if destData == nil || !bytes.Equal(srcData, destData) { + acl, err := aclOracle(destFile) + if err != nil { + return err + } + glog.V(2).Infof("Copying data from %s to %s", srcFile, destFile) - err = destFile.WriteFile(srcData) + err = destFile.WriteFile(srcData, acl) if err != nil { return fmt.Errorf("error writing dest file %q: %v", destFile, err) }