Skip to content

Commit

Permalink
Merge pull request #29 from hashmap-kz/fix/gitignorecmd
Browse files Browse the repository at this point in the history
gitignorecmd - fix gitignore
  • Loading branch information
hashmap-kz authored Jan 10, 2025
2 parents 20d76f9 + 81b7201 commit 0120dfa
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ avatars/
backups/
cover.out
coverage.txt
kubectl-envsubst

155 changes: 155 additions & 0 deletions cmd/kubectl-envsubst/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package main

import (
"fmt"
"io"
"os"
"os/exec"
"strings"

"github.com/hashmap-kz/kubectl-envsubst/pkg/cmd"
"github.com/hashmap-kz/kubectl-envsubst/pkg/version"
)

func main() {
err := runApp()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%s", err.Error())
os.Exit(1)
}
}

// runApp executes the plugin, with logic divided into smaller, testable components
func runApp() error {
// parse all passed cmd arguments without any modification
flags, err := cmd.ParseArgs()
if err != nil {
return err
}

// show help message
if flags.Help {
fmt.Println(cmd.UsageMessage)
return nil
}

// show version
if flags.Version {
fmt.Println(version.Version)
return nil
}

// 'apply' was not provided
if len(flags.Others) == 0 {
fmt.Println(cmd.UsageMessage)
return nil
}

// support apply operation only
if flags.Others[0] != "apply" {
fmt.Println(cmd.UsageMessage)
return nil
}

// it checks that executable exists
kubectl, err := exec.LookPath("kubectl")
if err != nil {
return err
}

// resolve all filenames: expand all glob-patterns, list directories, etc...
files, err := cmd.ResolveAllFiles(flags.Filenames, flags.Recursive)
if err != nil {
return err
}

// apply STDIN (if any)
if flags.HasStdin {
err := applyStdin(&flags, kubectl)
if err != nil {
return err
}
}

// apply passed files
for _, filename := range files {
err := applyOneFile(&flags, kubectl, filename)
if err != nil {
return err
}
}

return nil
}

// applyStdin substitutes content, passed to stdin `kubectl apply -f -`
func applyStdin(flags *cmd.ArgsRawRecognized, kubectl string) error {
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}

// substitute the whole stream of joined files at once
substitutedBuffer, err := substituteContent(flags, stdin)
if err != nil {
return err
}

return execKubectl(flags, kubectl, substitutedBuffer)
}

// applyOneFile read file (url, local-path), substitute its content, apply result
func applyOneFile(flags *cmd.ArgsRawRecognized, kubectl, filename string) error {
// recognize file type

var contentForSubst []byte
if cmd.IsURL(filename) {
data, err := cmd.ReadRemoteFileContent(filename)
if err != nil {
return err
}
contentForSubst = data
} else {
data, err := os.ReadFile(filename)
if err != nil {
return err
}
contentForSubst = data
}

// substitute the whole stream of joined files at once
substitutedBuffer, err := substituteContent(flags, contentForSubst)
if err != nil {
return err
}

return execKubectl(flags, kubectl, substitutedBuffer)
}

// substituteContent runs the subst module for a given content
func substituteContent(flags *cmd.ArgsRawRecognized, contentForSubst []byte) (string, error) {
envSubst := cmd.NewEnvsubst(flags.EnvsubstAllowedVars, flags.EnvsubstAllowedPrefix, true)
substitutedBuffer, err := envSubst.SubstituteEnvs(string(contentForSubst))
if err != nil {
return "", err
}
return substitutedBuffer, nil
}

// execKubectl applies a result buffer, bu running `kubectl apply -f -`
func execKubectl(flags *cmd.ArgsRawRecognized, kubectl, substitutedBuffer string) error {
// prepare kubectl args
args := []string{}
args = append(args, flags.Others...)
args = append(args, "-f", "-")

// pass stream of files to stdin
execCmd, err := cmd.ExecWithStdin(kubectl, []byte(substitutedBuffer), args...)
if err != nil {
fmt.Println(strings.TrimSpace(execCmd.StderrContent))
return err
}

fmt.Println(strings.TrimSpace(execCmd.StdoutContent))
return nil
}
109 changes: 109 additions & 0 deletions cmd/kubectl-envsubst/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package main

import (
"io"
"os"
"strings"
"testing"
)

// NOTE: This package contains integration tests.
//
// Key Characteristics:
// 1. **No Mocks**: These tests interact with actual external dependencies rather than mocked versions.
// The goal is to simulate real-world conditions as closely as possible.
// 2. **Sandboxed Environment**: To ensure safety and consistency, these tests are executed in a controlled and isolated environment (a kind-cluster sandbox).
// This prevents unintended side effects on production systems, local configurations, or external resources.
//
// Safeguards:
// - Before each test run, appropriate safeguards are implemented to validate the environment and prevent harmful actions.
//
// Expectations:
// - These tests may take longer to execute compared to unit tests because they involve real components.
//

const (
integrationTestEnv = "KUBECTL_ENVSUBST_INTEGRATION_TESTS_AVAILABLE"
integrationTestFlag = "0xcafebabe"
)

func TestApp_ApplyFromStdin(t *testing.T) {
if os.Getenv(integrationTestEnv) != integrationTestFlag {
t.Log("integration test was skipped due to configuration")
return
}

os.Args = []string{
"kubectl-envsubst", "apply", "-f", "-",
}

// Create a temporary file
tempFile, err := os.CreateTemp("", "mock-stdin-*")
if err != nil {
t.Fatal("Failed to create temporary file:", err)
}
defer os.Remove(tempFile.Name()) // Clean up the file afterwards

// Write mock input to the temporary file
mockInput := `
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-c4a3d54857ef43398b9a557050a7c83c
data:
key1: value1
`
if _, err := tempFile.Write([]byte(mockInput)); err != nil {
t.Fatal("Failed to write to temporary file:", err)
}

// Reset the file offset to the beginning for reading
if _, err := tempFile.Seek(0, io.SeekStart); err != nil {
t.Fatal("Failed to seek temporary file:", err)
}

// Replace os.Stdin with the temporary file
originalStdin := os.Stdin
defer func() { os.Stdin = originalStdin }()
os.Stdin = tempFile

// Create a temporary file for capturing stdout
stdoutFile, err := os.CreateTemp("", "mock-stdout-*")
if err != nil {
t.Fatalf("Failed to create temporary stdout file: %v", err)
}
defer os.Remove(stdoutFile.Name()) // Clean up the temp file after the test

// Replace os.Stdout with the temp file
originalStdout := os.Stdout
defer func() { os.Stdout = originalStdout }()
os.Stdout = stdoutFile

// Run application
err = runApp()
if err != nil {
t.Fatal(err)
}

// Flush stdout and reset pointer for reading
os.Stdout.Sync()
_, err = stdoutFile.Seek(0, io.SeekStart)
if err != nil {
t.Fatalf("Failed to seek 0. Stdout file.")
}

// Read and validate stdout content
output, err := io.ReadAll(stdoutFile)
if err != nil {
t.Fatalf("Failed to read from temporary stdout file: %v", err)
}

// capture expected output
strOut := string(output)
if !strings.Contains(strOut, "configmap/cm-c4a3d54857ef43398b9a557050a7c83c") {
t.Errorf("expected 'configmap/cm-c4a3d54857ef43398b9a557050a7c83c', got: %s", strOut)
}

t.Log(strOut)
}

0 comments on commit 0120dfa

Please sign in to comment.