Skip to content

Commit

Permalink
feat: Add ability to install unpacked artifacts in updater (#562)
Browse files Browse the repository at this point in the history
* start artifact install logic

* fix uninstall service step

* add tests for windows service manager

* remove kardiano/service dependency

* check filepath with spaces

* more tests, hook up to main

* naming

* add licenses

* gosec fixes

* linux gosec + some lint issues

* linter

* fix formatting of windows service test

* actually fix formatting

* guard linux/win service tests behind tag

* run tests as sudo on linux

* fix inverted conditional

* split updater integration tests into separate target

* refactor package for better encapsulation

* update darwin service to load/unload for start/stop

* fix installDir for windows after rename

* test replaceInstallDir

* add license to service_test.go

* fix make target phony

* add some comments

* add start of readme

* add a (very basic) readme

* use switch instead of multiple ifs

* Add comments to moveFiles

* fix failing darwin test
  • Loading branch information
BinaryFissionGames authored and StefanKurek committed Aug 4, 2022
1 parent 22b4b7d commit 788016e
Show file tree
Hide file tree
Showing 27 changed files with 2,106 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,9 @@ jobs:
${{ runner.os }}-go-
- name: Run Tests
run: make test
- name: Run Updater Integration Tests (non-linux)
if: matrix.os != 'ubuntu-20.04'
run: make test-updater-integration
- name: Run Updater Integration Tests (linux)
if: matrix.os == 'ubuntu-20.04'
run: sudo make test-updater-integration
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ OUTDIR=./dist
GOOS ?= $(shell go env GOOS)
GOARCH ?= $(shell go env GOARCH)

INTEGRATION_TEST_ARGS?=-tags integration

ifeq ($(GOOS), windows)
EXT?=.exe
else
Expand Down Expand Up @@ -109,6 +111,10 @@ test-with-cover:
$(MAKE) for-all CMD="go test -coverprofile=cover.out ./..."
$(MAKE) for-all CMD="go tool cover -html=cover.out -o cover.html"

.PHONY: test-updater-integration
test-updater-integration:
cd updater; go test $(INTEGRATION_TEST_ARGS) -race ./...

.PHONY: bench
bench:
$(MAKE) for-all CMD="go test -benchmem -run=^$$ -bench ^* ./..."
Expand All @@ -128,7 +134,8 @@ tidy:
.PHONY: gosec
gosec:
gosec -exclude-dir updater ./...
cd updater; gosec ./...
# exclude the testdata dir; it contains a go program for testing.
cd updater; gosec -exclude-dir internal/install/testdata ./...

# This target performs all checks that CI will do (excluding the build itself)
.PHONY: ci-checks
Expand Down
4 changes: 4 additions & 0 deletions updater/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# logging.yaml and manager.yaml are ignored in the base module, but
# they are important in this module for test data.
!logging.yaml
!manager.yaml
5 changes: 5 additions & 0 deletions updater/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# observIQ Distro for OpenTelemetry Updater

The updater is a separate binary that runs as a separate process to update collector artifacts (including the collector itself) when managed by [BindPlane OP](https://github.com/observIQ/bindplane-op).

Because the updater edits service configurations, it needs elevated privileges to run (root on Linux + macOS, administrative privileges on Windows).
9 changes: 9 additions & 0 deletions updater/cmd/updater/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"os"

"github.com/observiq/observiq-otel-collector/updater/internal/download"
"github.com/observiq/observiq-otel-collector/updater/internal/install"
"github.com/observiq/observiq-otel-collector/updater/internal/version"
"github.com/spf13/pflag"
)
Expand Down Expand Up @@ -61,4 +62,12 @@ func main() {
log.Fatalf("Failed to download and verify update: %s", err)
}

installer, err := install.NewInstaller(*tmpDir)
if err != nil {
log.Fatalf("Failed to create installer: %s", err)
}

if err := installer.Install(); err != nil {
log.Fatalf("Failed to install: %s", err)
}
}
3 changes: 3 additions & 0 deletions updater/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ module github.com/observiq/observiq-otel-collector/updater
go 1.17

require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mholt/archiver/v3 v3.5.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.2
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211
)

require (
Expand All @@ -18,6 +20,7 @@ require (
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.1.0 // indirect
github.com/ulikunitz/xz v0.5.9 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
5 changes: 5 additions & 0 deletions updater/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
Expand All @@ -25,6 +27,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
Expand All @@ -33,6 +36,8 @@ github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
172 changes: 172 additions & 0 deletions updater/internal/install/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright observIQ, Inc.
//
// 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 install

import (
"fmt"
"io"
"io/fs"
"log"
"os"
"path/filepath"
)

// Installer allows you to install files from latestDir into installDir,
// as well as update the service configuration using the "Install" method.
type Installer struct {
latestDir string
installDir string
svc Service
}

// NewInstaller returns a new instance of an Installer.
func NewInstaller(tempDir string) (*Installer, error) {
latestDir := filepath.Join(tempDir, "latest")
installDirPath, err := installDir()
if err != nil {
return nil, fmt.Errorf("failed to determine install dir: %w", err)
}

return &Installer{
latestDir: latestDir,
svc: newService(latestDir),
installDir: installDirPath,
}, nil
}

// Install installs the unpacked artifacts in latestDirPath to installDirPath,
// as well as installing the new service file using the provided Service interface
func (i Installer) Install() error {
// Stop service
if err := i.svc.Stop(); err != nil {
return fmt.Errorf("failed to stop service: %w", err)
}

// install files that go to installDirPath to their correct location,
// excluding any config files (logging.yaml, config.yaml, manager.yaml)
if err := moveFiles(i.latestDir, i.installDir); err != nil {
return fmt.Errorf("failed to install new files: %w", err)
}

// Uninstall previous service
if err := i.svc.Uninstall(); err != nil {
return fmt.Errorf("failed to uninstall service: %w", err)
}

// Install new service
if err := i.svc.Install(); err != nil {
return fmt.Errorf("failed to install service: %w", err)
}

// Start service
if err := i.svc.Start(); err != nil {
return fmt.Errorf("failed to start service: %w", err)
}

return nil
}

// moveFiles moves the file tree rooted at latestDirPath to installDirPath,
// skipping configuration files
func moveFiles(latestDirPath, installDirPath string) error {
err := filepath.WalkDir(latestDirPath, func(path string, d fs.DirEntry, err error) error {
switch {
case err != nil:
// if there was an error walking the directory, we want to bail out.
return err
case d.IsDir():
// Skip directories, we'll create them when we get a file in the directory.
return nil
case skipFile(path):
// Found a config file that we should skip copying.
return nil
}

cleanPath := filepath.Clean(path)

// We want the path relative to the directory we are walking in order to calculate where the file should be
// mirrored in the destination directory.
relPath, err := filepath.Rel(latestDirPath, cleanPath)
if err != nil {
return err
}

// use the relative path to get the outPath (where we should write the file), and
// to get the out directory (which we will create if it does not exist).
outPath := filepath.Clean(filepath.Join(installDirPath, relPath))
outDir := filepath.Dir(outPath)

if err := os.MkdirAll(outDir, 0750); err != nil {
return fmt.Errorf("failed to create dir: %w", err)
}

// Open the output file, creating it if it does not exist and truncating it.
outFile, err := os.OpenFile(outPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return fmt.Errorf("failed to open output file: %w", err)
}
defer func() {
err := outFile.Close()
if err != nil {
log.Default().Printf("installFiles: Failed to close output file: %s", err)
}
}()

// Open the input file for reading.
inFile, err := os.Open(cleanPath)
if err != nil {
return fmt.Errorf("failed to open input file: %w", err)
}
defer func() {
err := inFile.Close()
if err != nil {
log.Default().Printf("installFiles: Failed to close input file: %s", err)
}
}()

// Copy the input file to the output file.
if _, err := io.Copy(outFile, inFile); err != nil {
return fmt.Errorf("failed to copy file: %w", err)
}

return nil
})

if err != nil {
return fmt.Errorf("failed to walk latest dir: %w", err)
}

return nil
}

// skipFile returns true if the given path is a special config file.
// These files should not be overwritten.
func skipFile(path string) bool {
var configFiles = []string{
"config.yaml",
"logging.yaml",
"manager.yaml",
}

fileName := filepath.Base(path)

for _, f := range configFiles {
if fileName == f {
return true
}
}

return false
}
Loading

0 comments on commit 788016e

Please sign in to comment.