kubekey/pkg/service/provisioning/cloudinit/writefiles.go
24sama 3dd48dc8df refactor KubeKey project structure
Signed-off-by: 24sama <jacksama@foxmail.com>
2022-10-06 11:58:06 +08:00

245 lines
6.7 KiB
Go

/*
Copyright 2022 The KubeSphere Authors.
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 cloudinit
import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
"github.com/kubesphere/kubekey/pkg/clients/ssh"
"github.com/kubesphere/kubekey/pkg/service/operation"
"github.com/kubesphere/kubekey/pkg/service/operation/directory"
"github.com/kubesphere/kubekey/pkg/service/provisioning/commands"
"github.com/kubesphere/kubekey/pkg/util/filesystem"
)
const (
kubeadmInitPath = "/run/kubeadm/kubeadm.yaml"
kubeproxyComponentConfig = `
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
conntrack:
# Skip setting sysctl value "net.netfilter.nf_conntrack_max"
# It is a global variable that affects other namespaces
maxPerCore: 0
`
)
// writeFilesAction defines a list of files that should be written to a node.
type writeFilesAction struct {
sshClient ssh.Interface
directoryFactory func(sshClient ssh.Interface, path string, mode os.FileMode) operation.Directory
Files []files `json:"write_files,"`
}
type files struct {
Path string `json:"path,"`
Encoding string `json:"encoding,omitempty"`
Owner string `json:"owner,omitempty"`
Permissions string `json:"permissions,omitempty"`
Content string `json:"content,"`
Append bool `json:"append,"`
}
func newWriteFilesAction(sshClient ssh.Interface) action {
return &writeFilesAction{
sshClient: sshClient,
}
}
func (a *writeFilesAction) getDirectoryService(path string, mode os.FileMode) operation.Directory {
if a.directoryFactory != nil {
return a.directoryFactory(a.sshClient, path, mode)
}
return directory.NewService(a.sshClient, path, mode)
}
// Unmarshal unmarshals the given content into the given encoding.
func (a *writeFilesAction) Unmarshal(userData []byte) error {
if err := yaml.Unmarshal(userData, a); err != nil {
return errors.Wrapf(err, "error parsing write_files action: %s", userData)
}
return nil
}
// Commands return a list of commands to run on the node.
// Each command defines the parameters of a shell command necessary to generate a file replicating the cloud-init write_files module.
func (a *writeFilesAction) Commands() ([]commands.Cmd, error) {
cmds := make([]commands.Cmd, 0)
for _, f := range a.Files {
// Fix attributes and apply defaults
path := fixPath(f.Path) // NB. the real cloud init module for writes files converts path into absolute paths; this is not possible here...
encodings := fixEncoding(f.Encoding)
owner := fixOwner(f.Owner)
permissions := fixPermissions(f.Permissions)
content, err := fixContent(f.Content, encodings)
if path == kubeadmInitPath {
content += kubeproxyComponentConfig
}
if err != nil {
return cmds, errors.Wrapf(err, "error decoding content for %s", path)
}
// Make the directory so cat + redirection will work
directory := filepath.Dir(path)
cmds = append(cmds, commands.Cmd{Cmd: "mkdir", Args: []string{"-p", directory}})
redirects := ">"
if f.Append {
redirects = ">>"
}
// generate a command that will create a file with the expected contents.
cmds = append(cmds, commands.Cmd{Cmd: "/bin/sh", Args: []string{"-c", fmt.Sprintf("echo '%s' %s %s", content, redirects, path)}})
// if permissions are different than default ownership, add a command to modify the permissions.
if permissions != "0644" {
cmds = append(cmds, commands.Cmd{Cmd: "chmod", Args: []string{permissions, path}})
}
// if ownership is different than default ownership, add a command to modify file ownerhsip.
if owner != "root:root" {
cmds = append(cmds, commands.Cmd{Cmd: "chown", Args: []string{owner, path}})
}
}
return cmds, nil
}
// Run runs the action.
func (a *writeFilesAction) Run() error {
for _, f := range a.Files {
// Fix attributes and apply defaults
path := fixPath(f.Path) // NB. the real cloud init module for writes files converts path into absolute paths; this is not possible here...
encodings := fixEncoding(f.Encoding)
content, err := fixContent(f.Content, encodings)
if path == kubeadmInitPath {
content += kubeproxyComponentConfig
}
if err != nil {
return errors.Wrapf(err, "error decoding content for %s", path)
}
// Make the directory so cat + redirection will work
dir := filepath.Dir(path)
svc := a.getDirectoryService(dir, os.FileMode(filesystem.FileMode0755))
if err := svc.Make(); err != nil {
return err
}
if err := svc.Chown("root"); err != nil {
return err
}
redirects := ">"
if f.Append {
redirects = ">>"
}
if _, err := a.sshClient.SudoCmdf("echo '%s' %s %s", content, redirects, path); err != nil {
return err
}
}
return nil
}
func fixPath(p string) string {
return strings.TrimSpace(p)
}
func fixOwner(o string) string {
o = strings.TrimSpace(o)
if o != "" {
return o
}
return "root:root"
}
func fixPermissions(p string) string {
p = strings.TrimSpace(p)
if p != "" {
return p
}
return "0644"
}
func fixEncoding(e string) []string {
e = strings.ToLower(e)
e = strings.TrimSpace(e)
switch e {
case "gz", "gzip":
return []string{"application/x-gzip"}
case "gz+base64", "gzip+base64", "gz+b64", "gzip+b64":
return []string{"application/base64", "application/x-gzip"}
case "base64", "b64":
return []string{"application/base64"}
}
return []string{"text/plain"}
}
func fixContent(content string, encodings []string) (string, error) {
for _, e := range encodings {
switch e {
case "application/base64":
rByte, err := base64.StdEncoding.DecodeString(content)
if err != nil {
return content, errors.WithStack(err)
}
return string(rByte), nil
case "application/x-gzip":
rByte, err := gUnzipData([]byte(content))
if err != nil {
return content, err
}
return string(rByte), nil
case "text/plain":
return content, nil
default:
return content, errors.Errorf("Unknown bootstrap data encoding: %q", content)
}
}
return content, nil
}
func gUnzipData(data []byte) ([]byte, error) {
var r io.Reader
var err error
b := bytes.NewBuffer(data)
r, err = gzip.NewReader(b)
if err != nil {
return nil, errors.WithStack(err)
}
var resB bytes.Buffer
_, err = resB.ReadFrom(r)
if err != nil {
return nil, errors.WithStack(err)
}
return resB.Bytes(), nil
}