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
}