Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
hashmap-kz committed Jan 2, 2025
2 parents eaafcb8 + 8ffef25 commit b56c702
Show file tree
Hide file tree
Showing 13 changed files with 375 additions and 18 deletions.
90 changes: 90 additions & 0 deletions integration/stdin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package integration

import (
"fmt"
"log"
"os"
"os/exec"
"strings"
"testing"
)

func TestEnvsubstIntegrationFromStdin(t *testing.T) {

if os.Getenv("KUBECTL_ENVSUBST_INTEGRATION_TESTS_AVAILABLE") != "KUBECTL_ENVSUBST_INTEGRATION_TESTS_AVAILABLE" {
log.Printf("Integration test was skipped due to configuration")
return
}

resourceName := RandomIdent(32)

// Setup environment variables that was used in substitution
os.Setenv("IMAGE_NAME", "nginx")
os.Setenv("IMAGE_TAG", "latest")
os.Setenv("CI_PROJECT_NAME", resourceName)
defer os.Unsetenv("IMAGE_NAME")
defer os.Unsetenv("IMAGE_TAG")
defer os.Unsetenv("CI_PROJECT_NAME")

// configure CLI
os.Setenv("ENVSUBST_ALLOWED_PREFIXES", "CI_,IMAGE_")
defer os.Unsetenv("ENVSUBST_ALLOWED_PREFIXES")

// Prepare input manifest
manifest := `
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: &app ${CI_PROJECT_NAME}
labels:
app: *app
spec:
replicas: 1
selector:
matchLabels:
app: *app
template:
metadata:
labels:
app: *app
spec:
containers:
- name: *app
image: $IMAGE_NAME:$IMAGE_TAG
imagePullPolicy: Always
`

// Run kubectl-envsubst
cmdEnvsubstApply := exec.Command("kubectl", "envsubst", "apply", "-f", "-")
cmdEnvsubstApply.Stdin = strings.NewReader(manifest)
output, err := cmdEnvsubstApply.CombinedOutput()
if err != nil {
t.Fatalf("Failed to run kubectl envsubst: %v, output: %s", err, string(output))
}
fmt.Println(string(output))

// Check result (it should be created/updated/unchanged, etc...)
expectedOutput := strings.Contains(string(output), fmt.Sprintf("deployment.apps/%s", resourceName))
if !expectedOutput {
t.Errorf("Expected substituted output to contain 'deployment.apps/%s', got %s", resourceName, string(output))
}

// Validate applied resource
validateCmd := exec.Command("kubectl", "get", "deployment", resourceName)
validateOutput, err := validateCmd.CombinedOutput()
if err != nil {
t.Fatalf("Failed to validate applied resource: %v, output: %s", err, string(validateOutput))
}
if !strings.Contains(string(validateOutput), resourceName) {
t.Errorf("Expected deployment 'kubectl-envsubst-integration-test' to exist, got %s", string(validateOutput))
}

// cleanup
cmdDelete := exec.Command("kubectl", "delete", "deploy", resourceName)
outputDel, err := cmdDelete.CombinedOutput()
if err != nil {
t.Fatalf("Failed to cleanup: %v, output: %s", err, string(output))
}
fmt.Println(string(outputDel))
}
18 changes: 18 additions & 0 deletions integration/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package integration

import (
"math/rand"
"strings"
"time"
)

func RandomIdent(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))

b := make([]byte, length)
for i := range b {
b[i] = charset[seededRand.Intn(len(charset))]
}
return strings.ToLower("I" + string(b))
}
13 changes: 8 additions & 5 deletions pkg/cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type CmdFlagsProxy struct {
Recursive bool
Help bool
Others []string
HasStdin bool
}

func ParseCmdFlags() (*CmdFlagsProxy, error) {
Expand All @@ -36,6 +37,7 @@ func ParseCmdFlags() (*CmdFlagsProxy, error) {
Recursive: recognized.Recursive,
Help: recognized.Help,
Others: recognized.Others,
HasStdin: recognized.HasStdin,
}

for _, f := range recognized.Filenames {
Expand Down Expand Up @@ -77,12 +79,8 @@ func resolveFilenames(path string, recursive bool) ([]string, error) {
var results []string

// Check if the path is a URL
isURL := func(s string) bool {
u, err := url.Parse(s)
return err == nil && u.Scheme != "" && u.Host != ""
}

if isURL(path) {
if IsURL(path) {
// Add URL directly to results
results = append(results, path)
} else if strings.Contains(path, "*") {
Expand Down Expand Up @@ -129,3 +127,8 @@ func resolveFilenames(path string, recursive bool) ([]string, error) {
sort.Strings(results)
return results, nil
}

func IsURL(s string) bool {
u, err := url.Parse(s)
return err == nil && u.Scheme != "" && u.Host != ""
}
90 changes: 81 additions & 9 deletions pkg/cmd/joins.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,105 @@ package cmd

import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)

func JoinFiles(flags *CmdFlagsProxy) ([]byte, error) {
buf := bytes.Buffer{}

totalFiles := len(flags.Filenames)
if flags.HasStdin {
totalFiles += 1
}
needSeparator := totalFiles > 1
const separator = "\n---\n"

// process STDIN
if flags.HasStdin {
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, err
}
substituted, err := substBuf(flags, stdin)
if err != nil {
return nil, err
}
buf.WriteString(substituted)
if needSeparator {
buf.WriteString(separator)
}
}

// process files
for _, f := range flags.Filenames {
// passed as URL
if IsURL(f) {
data, err := readRemote(f)
if err != nil {
return nil, err
}
substituted, err := substBuf(flags, data)
if err != nil {
return nil, err
}
buf.WriteString(substituted)
if needSeparator {
buf.WriteString(separator)
}
continue
}

// get file data
// plain file
file, err := os.ReadFile(f)
if err != nil {
return nil, err
}

// substitute environment variables
// strict mode is always ON
envSubst := NewEnvsubst(flags.EnvsubstAllowedVars, flags.EnvsubstAllowedPrefix, true)
substituted, err := envSubst.SubstituteEnvs(string(file))
substituted, err := substBuf(flags, file)
if err != nil {
return nil, err
}

buf.WriteString(substituted)
if len(flags.Filenames) > 1 {
buf.WriteString("\n---\n")
if needSeparator {
buf.WriteString(separator)
}
}

return buf.Bytes(), nil
}

func readRemote(url string) ([]byte, error) {

// Make the HTTP GET request
response, err := http.Get(url)
if err != nil {
return nil, err
}
defer response.Body.Close()

// Check for HTTP errors
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("cannot GET file content from: %s", url)
}

// Read the response body
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}

return body, nil
}

func substBuf(flags *CmdFlagsProxy, data []byte) (string, error) {
// substitute environment variables
// strict mode is always ON
envSubst := NewEnvsubst(flags.EnvsubstAllowedVars, flags.EnvsubstAllowedPrefix, true)
substituted, err := envSubst.SubstituteEnvs(string(data))
if err != nil {
return "", err
}
return substituted, nil
}
52 changes: 52 additions & 0 deletions pkg/cmd/joins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cmd

import (
"io"
"net/http"
"strings"
"testing"
)

func TestReadRemote(t *testing.T) {

t.Run("Successful Request", func(t *testing.T) {
mockHTTPResponse := "Remote file content"
http.DefaultClient = &http.Client{
Transport: roundTripper(func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(mockHTTPResponse)),
}
}),
}
result, err := readRemote("http://example.com/data")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if string(result) != mockHTTPResponse {
t.Errorf("Expected: %s, Got: %s", mockHTTPResponse, string(result))
}
})

t.Run("Failed Request", func(t *testing.T) {
http.DefaultClient = &http.Client{
Transport: roundTripper(func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: 404,
Body: io.NopCloser(strings.NewReader("Not Found")),
}
}),
}
_, err := readRemote("http://example.com/not-found")
if err == nil {
t.Error("Expected error but got none")
}
})
}

// Helper for mocking http.Client
type roundTripper func(req *http.Request) *http.Response

func (f roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
36 changes: 32 additions & 4 deletions pkg/cmd/rawargs.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type CmdArgsRawRecognized struct {
Recursive bool
Help bool
Others []string
HasStdin bool
}

func allEmpty(where []string) bool {
Expand Down Expand Up @@ -46,24 +47,41 @@ func parseArgs() (CmdArgsRawRecognized, error) {
if filenameGiven == "" {
return result, fmt.Errorf("missing value for flag %s", arg)
}
result.Filenames = append(result.Filenames, filenameGiven)
err := handleStdin(filenameGiven, &result)
if err != nil {
return result, err
}
if filenameGiven != "-" {
result.Filenames = append(result.Filenames, filenameGiven)
}

// -f=pod.yaml
case strings.HasPrefix(arg, "-f="):
filenameGiven := strings.TrimPrefix(arg, "-f=")
if filenameGiven == "" {
return result, fmt.Errorf("missing value for flag %s", arg)
}
result.Filenames = append(result.Filenames, filenameGiven)

err := handleStdin(filenameGiven, &result)
if err != nil {
return result, err
}
if filenameGiven != "-" {
result.Filenames = append(result.Filenames, filenameGiven)
}
// --filename pod.yaml -f pod.yaml
case arg == "--filename" || arg == "-f":
if i+1 < len(args) {
filenameGiven := args[i+1]
if filenameGiven == "" {
return result, fmt.Errorf("missing value for flag %s", arg)
}
result.Filenames = append(result.Filenames, filenameGiven)
err := handleStdin(filenameGiven, &result)
if err != nil {
return result, err
}
if filenameGiven != "-" {
result.Filenames = append(result.Filenames, filenameGiven)
}
i++ // Skip the next argument since it's the value
} else {
return result, fmt.Errorf("missing value for flag %s", arg)
Expand Down Expand Up @@ -148,3 +166,13 @@ func parseArgs() (CmdArgsRawRecognized, error) {

return result, nil
}

func handleStdin(filenameGiven string, result *CmdArgsRawRecognized) error {
if filenameGiven == "-" {
if result.HasStdin {
return fmt.Errorf("multiple redirection to stdin detected")
}
result.HasStdin = true
}
return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ kubectl config set-context --current --namespace="${APP_NAMESPACE}"

# expand and apply manifests
kubectl envsubst apply -f manifests/ --envsubst-allowed-prefixes=CI_,APP_,INFRA_

# restore context
kubectl config set-context --current --namespace="default"
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ export ENVSUBST_ALLOWED_PREFIXES='CI_,APP_,INFRA_'

# expand and apply manifests
kubectl envsubst apply -f manifests/

# restore context
kubectl config set-context --current --namespace="default"
Loading

0 comments on commit b56c702

Please sign in to comment.