-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathutil.go
207 lines (179 loc) · 8.91 KB
/
util.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Copyright 2023 Cloudbase Solutions SRL
//
// 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 cloudconfig
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/cloudbase/garm-provider-common/defaults"
"github.com/cloudbase/garm-provider-common/params"
"github.com/pkg/errors"
)
// CloudConfigSpec is a struct that holds extra specs that can be used to customize user data.
type CloudConfigSpec struct {
// RunnerInstallTemplate can be used to override the default runner install template.
// If used, the caller is responsible for the correctness of the template as well as the
// suitability of the template for the target OS.
RunnerInstallTemplate []byte `json:"runner_install_template,omitempty" jsonschema:"title=default runner install template,description=This option can be used to override the default runner install template. If used, the caller is responsible for the correctness of the template as well as the suitability of the template for the target OS. Use the extra_context extra spec if your template has variables in it that need to be expanded."`
// PreInstallScripts is a map of pre-install scripts that will be run before the
// runner install script. These will run as root and can be used to prep a generic image
// before we attempt to install the runner. The key of the map is the name of the script
// as it will be written to disk. The value is a byte array with the contents of the script.
//
// These scripts will be added and run in alphabetical order.
//
// On Linux, we will set the executable flag. On Windows, the name matters as Windows looks for an
// extension to determine if the file is an executable or not. In theory this can hold binaries,
// but in most cases this will most likely hold scripts. We do not currenly validate the payload,
// so it's up to the user what they upload here.
// Caution needs to be exercised when using this feature, as the total size of userdata is limited
// on most providers.
PreInstallScripts map[string][]byte `json:"pre_install_scripts,omitempty" jsonschema:"title=pre-install scripts,description= map of pre-install scripts that will be run before the runner install script. These will run as root and can be used to prep a generic image before we attempt to install the runner. The key of the map is the name of the script as it will be written to disk. The value is a byte array with the contents of the script."`
// ExtraContext is a map of extra context that will be passed to the runner install template.
ExtraContext map[string]string `json:"extra_context,omitempty" jsonschema:"title=map of extra context,description=Extra context that will be passed to the runner_install_template."`
}
func sortMapKeys(m map[string][]byte) []string {
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// GetSpecs returns the cloud config specific extra specs from the bootstrap params.
func GetSpecs(bootstrapParams params.BootstrapInstance) (CloudConfigSpec, error) {
var extraSpecs CloudConfigSpec
if len(bootstrapParams.ExtraSpecs) == 0 {
return extraSpecs, nil
}
if err := json.Unmarshal(bootstrapParams.ExtraSpecs, &extraSpecs); err != nil {
return CloudConfigSpec{}, errors.Wrap(err, "unmarshaling extra specs")
}
if extraSpecs.ExtraContext == nil {
extraSpecs.ExtraContext = map[string]string{}
}
if extraSpecs.PreInstallScripts == nil {
extraSpecs.PreInstallScripts = map[string][]byte{}
}
return extraSpecs, nil
}
// GetRunnerInstallScript returns the runner install script for the given bootstrap params.
// This function will return either the default script for the given OS type or will use the supplied template
// if one is provided.
func GetRunnerInstallScript(bootstrapParams params.BootstrapInstance, tools params.RunnerApplicationDownload, runnerName string) ([]byte, error) {
if tools.GetFilename() == "" {
return nil, fmt.Errorf("missing tools filename")
}
if tools.GetDownloadURL() == "" {
return nil, fmt.Errorf("missing tools download URL")
}
tempToken := tools.GetTempDownloadToken()
extraSpecs, err := GetSpecs(bootstrapParams)
if err != nil {
return nil, errors.Wrap(err, "getting specs")
}
installRunnerParams := InstallRunnerParams{
FileName: tools.GetFilename(),
DownloadURL: tools.GetDownloadURL(),
TempDownloadToken: tempToken,
MetadataURL: bootstrapParams.MetadataURL,
RunnerUsername: defaults.DefaultUser,
RunnerGroup: defaults.DefaultUser,
RepoURL: bootstrapParams.RepoURL,
RunnerName: runnerName,
RunnerLabels: strings.Join(bootstrapParams.Labels, ","),
CallbackURL: bootstrapParams.CallbackURL,
CallbackToken: bootstrapParams.InstanceToken,
GitHubRunnerGroup: bootstrapParams.GitHubRunnerGroup,
ExtraContext: extraSpecs.ExtraContext,
EnableBootDebug: bootstrapParams.UserDataOptions.EnableBootDebug,
UseJITConfig: bootstrapParams.JitConfigEnabled,
}
if bootstrapParams.CACertBundle != nil && len(bootstrapParams.CACertBundle) > 0 {
installRunnerParams.CABundle = string(bootstrapParams.CACertBundle)
}
installScript, err := InstallRunnerScript(installRunnerParams, bootstrapParams.OSType, string(extraSpecs.RunnerInstallTemplate))
if err != nil {
return nil, errors.Wrap(err, "generating script")
}
return installScript, nil
}
// GetCloudInitConfig returns the cloud-init specific userdata config. This config can be used on most clouds
// for most Linux machines. The install runner script must be generated separately either by GetRunnerInstallScript()
// or some other means.
func GetCloudInitConfig(bootstrapParams params.BootstrapInstance, installScript []byte) (string, error) {
extraSpecs, err := GetSpecs(bootstrapParams)
if err != nil {
return "", errors.Wrap(err, "getting specs")
}
cloudCfg := NewDefaultCloudInitConfig()
if bootstrapParams.UserDataOptions.DisableUpdatesOnBoot {
cloudCfg.PackageUpgrade = false
cloudCfg.Packages = []string{}
}
for _, pkg := range bootstrapParams.UserDataOptions.ExtraPackages {
cloudCfg.AddPackage(pkg)
}
if len(extraSpecs.PreInstallScripts) > 0 {
names := sortMapKeys(extraSpecs.PreInstallScripts)
for _, name := range names {
script := extraSpecs.PreInstallScripts[name]
cloudCfg.AddFile(script, fmt.Sprintf("/garm-pre-install/%s", name), "root:root", "755")
cloudCfg.AddRunCmd(fmt.Sprintf("/garm-pre-install/%s", name))
}
}
cloudCfg.AddRunCmd("rm -rf /garm-pre-install")
cloudCfg.AddSSHKey(bootstrapParams.SSHKeys...)
cloudCfg.AddFile(installScript, "/install_runner.sh", "root:root", "755")
cloudCfg.AddRunCmd(fmt.Sprintf("su -l -c /install_runner.sh %s", defaults.DefaultUser))
cloudCfg.AddRunCmd("rm -f /install_runner.sh")
if bootstrapParams.CACertBundle != nil && len(bootstrapParams.CACertBundle) > 0 {
if err := cloudCfg.AddCACert(bootstrapParams.CACertBundle); err != nil {
return "", errors.Wrap(err, "adding CA cert bundle")
}
}
asStr, err := cloudCfg.Serialize()
if err != nil {
return "", errors.Wrap(err, "creating cloud config")
}
return asStr, nil
}
// GetCloudConfig is a helper function that generates a cloud-init config for Linux and a powershell script for Windows.
// In most cases this function should do, but in situations where a more custom approach is needed, you may need to call
// GetCloudInitConfig() or GetRunnerInstallScript() directly and compose the final userdata in a different way.
// The extra specs PreInstallScripts is only supported on Linux via cloud-init by this function. On some providers, like Azure
// Windows initialization scripts are run by creating a separate CustomScriptExtension resource for each individual script.
// On other clouds it may be different. This function aims to be generic, which is why it only supports the PreInstallScripts
// via cloud-init.
func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools params.RunnerApplicationDownload, runnerName string) (string, error) {
installScript, err := GetRunnerInstallScript(bootstrapParams, tools, runnerName)
if err != nil {
return "", errors.Wrap(err, "generating script")
}
var asStr string
switch bootstrapParams.OSType {
case params.Linux:
cloudCfg, err := GetCloudInitConfig(bootstrapParams, installScript)
if err != nil {
return "", errors.Wrap(err, "getting cloud init config")
}
return cloudCfg, nil
case params.Windows:
asStr = string(installScript)
default:
return "", fmt.Errorf("unknown os type: %s", bootstrapParams.OSType)
}
return asStr, nil
}