Files
coder/provisionersdk/archive.go
T
Spike Curtis bddb808b25 chore: arrange imports in a standard way (#21452)
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example:

```
import (
	"context"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/xerrors"
	"gopkg.in/natefinch/lumberjack.v2"

	"cdr.dev/slog/v3"
	"github.com/coder/coder/v2/codersdk/agentsdk"
	"github.com/coder/serpent"
)
```

3 groups: standard library, 3rd partly libs, Coder libs.

This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
2026-01-08 15:24:11 +04:00

195 lines
4.7 KiB
Go

package provisionersdk
import (
"archive/tar"
"context"
"io"
"os"
"path/filepath"
"strings"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd/util/xio"
)
const (
// TemplateArchiveLimit represents the maximum size of a template in bytes.
TemplateArchiveLimit = 1 << 20
)
func dirHasExt(dir string, exts ...string) (bool, error) {
dirEnts, err := os.ReadDir(dir)
if err != nil {
return false, err
}
for _, fi := range dirEnts {
for _, ext := range exts {
if strings.HasSuffix(fi.Name(), ext) {
return true, nil
}
}
}
return false, nil
}
func DirHasLockfile(dir string) (bool, error) {
return dirHasExt(dir, ".terraform.lock.hcl")
}
// Tar archives a Terraform directory.
func Tar(w io.Writer, logger slog.Logger, directory string, limit int64) error {
// The total bytes written must be under the limit, so use -1
w = xio.NewLimitWriter(w, limit-1)
tarWriter := tar.NewWriter(w)
tfExts := []string{".tf", ".tf.json"}
hasTf, err := dirHasExt(directory, tfExts...)
if err != nil {
return err
}
if !hasTf {
absPath, err := filepath.Abs(directory)
if err != nil {
return err
}
// Show absolute path to aid in debugging. E.g. showing "." is
// useless.
return xerrors.Errorf(
"%s is not a valid template since it has no %s files",
absPath, tfExts,
)
}
err = filepath.Walk(directory, func(file string, fileInfo os.FileInfo, err error) error {
if err != nil {
return err
}
var link string
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err = os.Readlink(file)
if err != nil {
return err
}
}
header, err := tar.FileInfoHeader(fileInfo, link)
if err != nil {
return err
}
rel, err := filepath.Rel(directory, file)
if err != nil {
return err
}
// We want to allow .terraform.lock.hcl files to be archived. This
// allows provider plugins to be cached.
if (strings.HasPrefix(rel, ".") || strings.HasPrefix(filepath.Base(rel), ".")) && filepath.Base(rel) != ".terraform.lock.hcl" {
if fileInfo.IsDir() && rel != "." {
// Don't archive hidden files!
return filepath.SkipDir
}
// Don't archive hidden files!
return nil
}
if strings.Contains(rel, ".tfstate") {
// Don't store tfstate!
logger.Debug(context.Background(), "skip state", slog.F("name", rel))
return nil
}
if rel == "terraform.tfvars" || rel == "terraform.tfvars.json" || strings.HasSuffix(rel, ".auto.tfvars") || strings.HasSuffix(rel, ".auto.tfvars.json") {
// Don't store .tfvars, as Coder uses their own variables file.
logger.Debug(context.Background(), "skip variable definitions", slog.F("name", rel))
return nil
}
// Use unix paths in the tar archive.
header.Name = filepath.ToSlash(rel)
// tar.FileInfoHeader() will do this, but filepath.Rel() calls filepath.Clean()
// which strips trailing path separators for directories.
if fileInfo.IsDir() {
header.Name += "/"
}
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
if !fileInfo.Mode().IsRegular() {
return nil
}
data, err := os.Open(file)
if err != nil {
return err
}
defer data.Close()
_, err = io.Copy(tarWriter, data)
if err != nil {
if xerrors.Is(err, xio.ErrLimitReached) {
return xerrors.Errorf("Archive too big. Must be <= %d bytes", limit)
}
return err
}
return data.Close()
})
if err != nil {
if xerrors.Is(err, xio.ErrLimitReached) {
return xerrors.Errorf("Archive too big. Must be <= %d bytes", limit)
}
return err
}
err = tarWriter.Flush()
if err != nil {
return err
}
return nil
}
// Untar extracts the archive to a provided directory.
func Untar(directory string, r io.Reader) error {
tarReader := tar.NewReader(r)
for {
header, err := tarReader.Next()
if xerrors.Is(err, io.EOF) {
return nil
}
if err != nil {
return err
}
if header.Name == "." || strings.Contains(header.Name, "..") {
continue
}
// #nosec
target := filepath.Join(directory, filepath.FromSlash(header.Name))
switch header.Typeflag {
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0o755); err != nil {
return err
}
}
case tar.TypeReg:
// #nosec G115 - Safe conversion as tar header mode fits within uint32
err := os.MkdirAll(filepath.Dir(target), os.FileMode(header.Mode)|os.ModeDir|100)
if err != nil {
return err
}
// #nosec G115 - Safe conversion as tar header mode fits within uint32
file, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
}
// Max file size of 10MB.
_, err = io.CopyN(file, tarReader, (1<<20)*10)
if xerrors.Is(err, io.EOF) {
err = nil
}
if err != nil {
return err
}
_ = file.Close()
}
}
}