| // Copyright 2020 Google Inc. All Rights Reserved. |
| // |
| // 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 config |
| |
| import ( |
| "context" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strconv" |
| "strings" |
| |
| "github.com/GoogleCloudPlatform/osconfig/clog" |
| "github.com/GoogleCloudPlatform/osconfig/util" |
| |
| "cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb" |
| ) |
| |
| const defaultFilePerms = 0644 |
| |
| type fileResource struct { |
| *agentendpointpb.OSPolicy_Resource_FileResource |
| |
| managedFile ManagedFile |
| } |
| |
| // ManagedFile is the file that this FileResouce manages. |
| type ManagedFile struct { |
| Path string |
| tempDir string |
| source string |
| checksum string |
| State agentendpointpb.OSPolicy_Resource_FileResource_DesiredState |
| Permisions os.FileMode |
| } |
| |
| func parsePermissions(s string) (os.FileMode, error) { |
| if s == "" { |
| return defaultFilePerms, nil |
| } |
| |
| i, err := strconv.ParseUint(s, 8, 32) |
| if err != nil { |
| return 0, err |
| } |
| return os.FileMode(i), nil |
| } |
| |
| // TODO: use a persistent cache for downloaded files so we dont need to redownload them each time. |
| func (f *fileResource) download(ctx context.Context) error { |
| // No need to download if source is a local file. |
| if f.GetFile().GetLocalPath() != "" { |
| return nil |
| } |
| |
| tmpDir, err := ioutil.TempDir("", "osconfig_file_resource_") |
| if err != nil { |
| return fmt.Errorf("failed to create working dir: %s", err) |
| } |
| f.managedFile.tempDir = tmpDir |
| |
| tmpFile := filepath.Join(tmpDir, filepath.Base(f.GetPath())) |
| f.managedFile.source = tmpFile |
| perms := os.FileMode(0644) |
| |
| switch f.GetSource().(type) { |
| case *agentendpointpb.OSPolicy_Resource_FileResource_Content: |
| f.managedFile.checksum, err = util.AtomicWriteFileStream(strings.NewReader(f.GetContent()), "", tmpFile, perms) |
| if err != nil { |
| return err |
| } |
| |
| case *agentendpointpb.OSPolicy_Resource_FileResource_File: |
| f.managedFile.checksum, err = downloadFile(ctx, tmpFile, perms, f.GetFile()) |
| if err != nil { |
| return err |
| } |
| default: |
| return fmt.Errorf("unrecognized Source type for FileResource: %q", f.GetSource()) |
| } |
| |
| return nil |
| } |
| |
| func (f *fileResource) validate(ctx context.Context) (*ManagedResources, error) { |
| switch f.GetState() { |
| case agentendpointpb.OSPolicy_Resource_FileResource_ABSENT, agentendpointpb.OSPolicy_Resource_FileResource_PRESENT, agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH: |
| f.managedFile.State = f.GetState() |
| default: |
| return nil, fmt.Errorf("unrecognized DesiredState for FileResource: %q", f.GetState()) |
| } |
| |
| f.managedFile.Path = f.GetPath() |
| |
| // If desired state is absent, we can return now. |
| if f.GetState() == agentendpointpb.OSPolicy_Resource_FileResource_ABSENT { |
| return &ManagedResources{Files: []ManagedFile{f.managedFile}}, nil |
| } |
| |
| perms, err := parsePermissions(f.GetPermissions()) |
| if err != nil { |
| return nil, fmt.Errorf("can't parse permissions %q: %v", f.GetPermissions(), err) |
| } |
| f.managedFile.Permisions = perms |
| |
| if f.GetFile().GetLocalPath() != "" { |
| f.managedFile.source = f.GetFile().GetLocalPath() |
| file, err := os.Open(f.GetFile().GetLocalPath()) |
| if err != nil { |
| return nil, err |
| } |
| f.managedFile.checksum = checksum(file) |
| file.Close() |
| } |
| |
| switch f.managedFile.State { |
| case agentendpointpb.OSPolicy_Resource_FileResource_ABSENT: |
| case agentendpointpb.OSPolicy_Resource_FileResource_PRESENT: |
| // If the file is already present no need to downloaded it. |
| if !util.Exists(f.managedFile.Path) { |
| if err := f.download(ctx); err != nil { |
| return nil, err |
| } |
| } |
| case agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH: |
| if err := f.download(ctx); err != nil { |
| return nil, err |
| } |
| default: |
| return nil, fmt.Errorf("unrecognized DesiredState for FileResource: %q", f.managedFile.State) |
| } |
| |
| return &ManagedResources{Files: []ManagedFile{f.managedFile}}, nil |
| } |
| |
| func (f *fileResource) checkState(ctx context.Context) (inDesiredState bool, err error) { |
| switch f.managedFile.State { |
| case agentendpointpb.OSPolicy_Resource_FileResource_ABSENT: |
| return !util.Exists(f.managedFile.Path), nil |
| case agentendpointpb.OSPolicy_Resource_FileResource_PRESENT: |
| return util.Exists(f.managedFile.Path), nil |
| case agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH: |
| return contentsMatch(ctx, f.managedFile.Path, f.managedFile.checksum) |
| default: |
| return false, fmt.Errorf("unrecognized DesiredState for FileResource: %q", f.managedFile.State) |
| } |
| } |
| |
| func copyFile(dst, src string, perms os.FileMode) (retErr error) { |
| reader, err := os.Open(src) |
| if err != nil { |
| return fmt.Errorf("error opening source file: %v", err) |
| } |
| defer reader.Close() |
| writer, err := os.OpenFile(dst, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, perms) |
| if err != nil { |
| return fmt.Errorf("error opening destination file: %v", err) |
| } |
| defer func() { |
| if err := writer.Close(); err != nil { |
| if retErr == nil { |
| retErr = fmt.Errorf("error closing destination file: %v", err) |
| } |
| } |
| }() |
| |
| if _, err := io.Copy(writer, reader); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (f *fileResource) enforceState(ctx context.Context) (inDesiredState bool, err error) { |
| clog.Infof(ctx, "Enforcing state %q for file %q.", f.managedFile.State, f.managedFile.Path) |
| switch f.managedFile.State { |
| case agentendpointpb.OSPolicy_Resource_FileResource_ABSENT: |
| if err := os.Remove(f.managedFile.Path); err != nil { |
| return false, fmt.Errorf("error removing %q: %v", f.managedFile.Path, err) |
| } |
| case agentendpointpb.OSPolicy_Resource_FileResource_PRESENT, agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH: |
| // Download now if for some reason we got this point and have not. |
| if f.managedFile.source == "" { |
| if err := f.download(ctx); err != nil { |
| return false, err |
| } |
| } |
| if err := copyFile(f.managedFile.Path, f.managedFile.source, f.managedFile.Permisions); err != nil { |
| return false, fmt.Errorf("error copying %q to %q: %v", f.managedFile.source, f.managedFile.Path, err) |
| } |
| default: |
| return false, fmt.Errorf("unrecognized DesiredState for FileResource: %q", f.managedFile.State) |
| } |
| |
| return true, nil |
| } |
| |
| func (f *fileResource) populateOutput(rCompliance *agentendpointpb.OSPolicyResourceCompliance) {} |
| |
| func (f *fileResource) cleanup(ctx context.Context) error { |
| if f.managedFile.tempDir != "" { |
| return os.RemoveAll(f.managedFile.tempDir) |
| } |
| return nil |
| } |