config/repository_resource.go (249 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 ( "bytes" "context" "errors" "fmt" "io" "io/ioutil" "net/http" "os" "path/filepath" "github.com/GoogleCloudPlatform/osconfig/agentconfig" "github.com/GoogleCloudPlatform/osconfig/clog" "github.com/GoogleCloudPlatform/osconfig/packages" "github.com/GoogleCloudPlatform/osconfig/util" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/armor" "cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb" ) const aptGPGDir = "/etc/apt/trusted.gpg.d" type repositoryResource struct { *agentendpointpb.OSPolicy_Resource_RepositoryResource managedRepository ManagedRepository } // AptRepository describes an apt repository resource. type AptRepository struct { RepositoryResource *agentendpointpb.OSPolicy_Resource_RepositoryResource_AptRepository GpgFilePath string GpgChecksum string GpgFileContents []byte } // GooGetRepository describes an googet repository resource. type GooGetRepository struct { RepositoryResource *agentendpointpb.OSPolicy_Resource_RepositoryResource_GooRepository } // YumRepository describes an yum repository resource. type YumRepository struct { RepositoryResource *agentendpointpb.OSPolicy_Resource_RepositoryResource_YumRepository } // ZypperRepository describes an zypper repository resource. type ZypperRepository struct { RepositoryResource *agentendpointpb.OSPolicy_Resource_RepositoryResource_ZypperRepository } // ManagedRepository is the repository that this RepositoryResource manages. type ManagedRepository struct { Apt *AptRepository GooGet *GooGetRepository Yum *YumRepository Zypper *ZypperRepository RepoFilePath string RepoChecksum string RepoFileContents []byte } func aptRepoContents(repo *agentendpointpb.OSPolicy_Resource_RepositoryResource_AptRepository) []byte { var debArchiveTypeMap = map[agentendpointpb.OSPolicy_Resource_RepositoryResource_AptRepository_ArchiveType]string{ agentendpointpb.OSPolicy_Resource_RepositoryResource_AptRepository_DEB: "deb", agentendpointpb.OSPolicy_Resource_RepositoryResource_AptRepository_DEB_SRC: "deb-src", } /* # Repo file managed by Google OSConfig agent deb http://repo1-url/ repo main */ var buf bytes.Buffer buf.WriteString("# Repo file managed by Google OSConfig agent\n") archiveType, ok := debArchiveTypeMap[repo.GetArchiveType()] if !ok { archiveType = "deb" } line := fmt.Sprintf("%s %s %s", archiveType, repo.GetUri(), repo.GetDistribution()) for _, c := range repo.GetComponents() { line = fmt.Sprintf("%s %s", line, c) } buf.WriteString(line + "\n") return buf.Bytes() } func googetRepoContents(repo *agentendpointpb.OSPolicy_Resource_RepositoryResource_GooRepository) []byte { /* # Repo file managed by Google OSConfig agent - name: repo1-name url: https://repo1-url */ var buf bytes.Buffer buf.WriteString("# Repo file managed by Google OSConfig agent\n") buf.WriteString(fmt.Sprintf("- name: %s\n", repo.Name)) buf.WriteString(fmt.Sprintf(" url: %s\n", repo.Url)) return buf.Bytes() } func yumRepoContents(repo *agentendpointpb.OSPolicy_Resource_RepositoryResource_YumRepository) []byte { /* # Repo file managed by Google OSConfig agent [Id] name=DisplayName baseurl=https://repo-url enabled=1 gpgcheck=1 gpgkey=http://repo-url/gpg1 http://repo-url/gpg2 */ var buf bytes.Buffer buf.WriteString("# Repo file managed by Google OSConfig agent\n") buf.WriteString(fmt.Sprintf("[%s]\n", repo.Id)) if repo.DisplayName == "" { buf.WriteString(fmt.Sprintf("name=%s\n", repo.Id)) } else { buf.WriteString(fmt.Sprintf("name=%s\n", repo.DisplayName)) } buf.WriteString(fmt.Sprintf("baseurl=%s\n", repo.BaseUrl)) buf.WriteString("enabled=1\ngpgcheck=1\n") if len(repo.GpgKeys) > 0 { buf.WriteString(fmt.Sprintf("gpgkey=%s\n", repo.GpgKeys[0])) for _, k := range repo.GpgKeys[1:] { buf.WriteString(fmt.Sprintf(" %s\n", k)) } } return buf.Bytes() } func zypperRepoContents(repo *agentendpointpb.OSPolicy_Resource_RepositoryResource_ZypperRepository) []byte { /* # Repo file managed by Google OSConfig agent [Id] name=DisplayName baseurl=https://repo-url enabled=1 gpgkey=https://repo-url/gpg1 https://repo-url/gpg2 */ var buf bytes.Buffer buf.WriteString("# Repo file managed by Google OSConfig agent\n") buf.WriteString(fmt.Sprintf("[%s]\n", repo.Id)) if repo.DisplayName == "" { buf.WriteString(fmt.Sprintf("name=%s\n", repo.Id)) } else { buf.WriteString(fmt.Sprintf("name=%s\n", repo.DisplayName)) } buf.WriteString(fmt.Sprintf("baseurl=%s\n", repo.BaseUrl)) buf.WriteString("enabled=1\n") if len(repo.GpgKeys) > 0 { buf.WriteString(fmt.Sprintf("gpgkey=%s\n", repo.GpgKeys[0])) for _, k := range repo.GpgKeys[1:] { buf.WriteString(fmt.Sprintf(" %s\n", k)) } } return buf.Bytes() } func serializeGPGKeyEntity(entityList openpgp.EntityList) ([]byte, error) { var buf bytes.Buffer for _, entity := range entityList { if err := entity.Serialize(&buf); err != nil { return nil, fmt.Errorf("error serializing gpg key: %v", err) } } return buf.Bytes(), nil } func isArmoredGPGKey(keyData []byte) bool { var buf bytes.Buffer tee := io.TeeReader(bytes.NewReader(keyData), &buf) // Try decoding as armored decodedBlock, err := armor.Decode(tee) if err == nil && decodedBlock != nil { return true } return false } func fetchGPGKey(key string) (openpgp.EntityList, error) { resp, err := http.Get(key) if err != nil { return nil, err } defer resp.Body.Close() if resp.ContentLength > 1024*1024 { return nil, fmt.Errorf("key size of %d too large", resp.ContentLength) } responseBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("can not read response body for key %s, err: %v", key, err) } if isArmoredGPGKey(responseBody) { return openpgp.ReadArmoredKeyRing(bytes.NewBuffer(responseBody)) } return openpgp.ReadKeyRing(bytes.NewReader(responseBody)) } func (r *repositoryResource) validate(ctx context.Context) (*ManagedResources, error) { var repoFormat string switch r.GetRepository().(type) { case *agentendpointpb.OSPolicy_Resource_RepositoryResource_Apt: if !packages.AptExists { return nil, errors.New("cannot manage Apt repository because apt-get does not exist on the system") } gpgkey := r.GetApt().GetGpgKey() r.managedRepository.Apt = &AptRepository{RepositoryResource: r.GetApt()} r.managedRepository.RepoFileContents = aptRepoContents(r.GetApt()) repoFormat = agentconfig.AptRepoFormat() if gpgkey != "" { entityList, err := fetchGPGKey(gpgkey) if err != nil { return nil, fmt.Errorf("error fetching apt gpg key %q: %v", gpgkey, err) } keyContents, err := serializeGPGKeyEntity(entityList) if err != nil { return nil, fmt.Errorf("error fetching apt gpg key %q: %v", gpgkey, err) } r.managedRepository.Apt.GpgFileContents = keyContents r.managedRepository.Apt.GpgChecksum = checksum(bytes.NewReader(keyContents)) r.managedRepository.Apt.GpgFilePath = filepath.Join(aptGPGDir, "osconfig_added_"+r.managedRepository.Apt.GpgChecksum+".gpg") } case *agentendpointpb.OSPolicy_Resource_RepositoryResource_Goo: if !packages.GooGetExists { return nil, errors.New("cannot manage googet repository because googet does not exist on the system") } r.managedRepository.GooGet = &GooGetRepository{RepositoryResource: r.GetGoo()} r.managedRepository.RepoFileContents = googetRepoContents(r.GetGoo()) repoFormat = agentconfig.GooGetRepoFormat() case *agentendpointpb.OSPolicy_Resource_RepositoryResource_Yum: if !packages.YumExists { return nil, errors.New("cannot manage yum repository because yum does not exist on the system") } r.managedRepository.Yum = &YumRepository{RepositoryResource: r.GetYum()} r.managedRepository.RepoFileContents = yumRepoContents(r.GetYum()) repoFormat = agentconfig.YumRepoFormat() case *agentendpointpb.OSPolicy_Resource_RepositoryResource_Zypper: if !packages.ZypperExists { return nil, errors.New("cannot manage zypper repository because zypper does not exist on the system") } r.managedRepository.Zypper = &ZypperRepository{RepositoryResource: r.GetZypper()} r.managedRepository.RepoFileContents = zypperRepoContents(r.GetZypper()) repoFormat = agentconfig.ZypperRepoFormat() default: return nil, fmt.Errorf("Repository field not set or references unknown repository type: %v", r.GetRepository()) } r.managedRepository.RepoChecksum = checksum(bytes.NewReader(r.managedRepository.RepoFileContents)) r.managedRepository.RepoFilePath = fmt.Sprintf(repoFormat, r.managedRepository.RepoChecksum[:10]) return &ManagedResources{Repositories: []ManagedRepository{r.managedRepository}}, nil } func contentsMatch(ctx context.Context, path, chksum string) (bool, error) { file, err := os.OpenFile(path, os.O_RDONLY, 0644) if err != nil { if os.IsNotExist(err) { clog.Debugf(ctx, "File not found: %s", path) return false, nil } clog.Debugf(ctx, "Error opening file %s.", path) return false, err } defer file.Close() actualChecksum := checksum(file) if actualChecksum != chksum { clog.Debugf(ctx, "Checksums don't match, got: %s, actual: %s", chksum, actualChecksum) return false, nil } return true, nil } func (r *repositoryResource) checkState(ctx context.Context) (inDesiredState bool, err error) { // Check APT gpg key if applicable. if r.managedRepository.Apt != nil && r.managedRepository.Apt.GpgFileContents != nil { match, err := contentsMatch(ctx, r.managedRepository.Apt.GpgFilePath, r.managedRepository.Apt.GpgChecksum) if err != nil { return false, err } if !match { return false, nil } } return contentsMatch(ctx, r.managedRepository.RepoFilePath, r.managedRepository.RepoChecksum) } func (r *repositoryResource) enforceState(ctx context.Context) (inDesiredState bool, err error) { clog.Infof(ctx, "Enforcing repo %s.", r.managedRepository.RepoFilePath) // Set APT gpg key if applicable. if r.managedRepository.Apt != nil && r.managedRepository.Apt.GpgFileContents != nil { if err := ioutil.WriteFile(r.managedRepository.Apt.GpgFilePath, r.managedRepository.Apt.GpgFileContents, 0644); err != nil { return false, err } } if err := os.MkdirAll(filepath.Dir(r.managedRepository.RepoFilePath), 0755); err != nil { return false, err } if err := util.AtomicWrite(r.managedRepository.RepoFilePath, r.managedRepository.RepoFileContents, 0644); err != nil { return false, err } return true, nil } func (r *repositoryResource) populateOutput(rCompliance *agentendpointpb.OSPolicyResourceCompliance) { } func (r *repositoryResource) cleanup(ctx context.Context) error { return nil }