config/file_resource.go (169 lines of code) (raw):
// 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
}