Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions acceptance/cache/exploratory/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bundle:
name: exploratory-cache-test
5 changes: 5 additions & 0 deletions acceptance/cache/exploratory/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions acceptance/cache/exploratory/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
unset DATABRICKS_CLIENT_SECRET
unset DATABRICKS_CLIENT_ID
unset DATABRICKS_HOST
unset DATABRICKS_AUTH_TYPE

# export DATABRICKS_CONFIG_FILE=/Users/<your-username>/.databrickscfg
trace $CLI bundle validate -p dogfood
2 changes: 2 additions & 0 deletions acceptance/cache/exploratory/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud=false
Local=false
35 changes: 34 additions & 1 deletion bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ import (
"github.com/hashicorp/terraform-exec/tfexec"
)

const internalFolder = ".internal"
const (
internalFolder = ".internal"
cacheFolder = ".cache"
)

// Filename where resources are stored for DATABRICKS_BUNDLE_ENGINE=direct
const resourcesFilename = "resources.json"
Expand Down Expand Up @@ -287,6 +290,36 @@ func (b *Bundle) InternalDir(ctx context.Context) (string, error) {
return dir, nil
}

// BundleLevelCacheDir is used to cache components needed for the bundle that are target-independent
func (b *Bundle) BundleLevelCacheDir(ctx context.Context, cacheComponentName string) (string, error) {
cacheDirName, exists := env.TempDir(ctx)
if !exists || cacheDirName == "" {
cacheDirName = filepath.Join(
// Anchor at bundle root directory.
b.BundleRootPath,
// Static cache directory.
".databricks",
)
}

// Fixed components of the result path.
parts := []string{
cacheDirName,
cacheFolder,
cacheComponentName,
}

// Make directory if it doesn't exist yet.
dir := filepath.Join(parts...)
err := os.MkdirAll(dir, 0o700)
if err != nil {
return "", err
}

libsync.WriteGitIgnore(ctx, b.BundleRootPath)
return dir, nil
}

// GetSyncIncludePatterns returns a list of user defined includes
// And also adds InternalDir folder to include list for sync command
// so this folder is always synced
Expand Down
120 changes: 120 additions & 0 deletions bundle/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package bundle

import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
)

// Cache provides an abstract interface for caching content to local disk.
// Implementations should handle storing and retrieving cached components
// using fingerprints for cache invalidation.
type Cache interface {
// Read retrieves cached content for the given fingerprint.
// Returns the cached data and true if found, or nil and false if not found or expired.
Read(ctx context.Context, fingerprint string) ([]byte, bool)

// Store saves content to the cache with the given fingerprint.
// Returns an error if the cache operation fails.
Store(ctx context.Context, fingerprint string, content []byte) error

// Clear removes all cached content from the cache directory.
Clear(ctx context.Context) error

// ClearFingerprint removes cached content for a specific fingerprint.
ClearFingerprint(ctx context.Context, fingerprint string) error
}

// FileCache implements the Cache interface using the local filesystem.
type FileCache struct {
cachePath string
}

// NewFileCache creates a new filesystem-based cache at the specified path.
func NewFileCache(cachePath string) *FileCache {
return &FileCache{
cachePath: cachePath,
}
}

// Read retrieves cached content for the given fingerprint.
func (fc *FileCache) Read(ctx context.Context, fingerprint string) ([]byte, bool) {
filePath := fc.getFilePath(fingerprint)
data, err := os.ReadFile(filePath)
if err != nil {
return nil, false
}

return data, true
}

// Store saves content to the cache with the given fingerprint.
func (fc *FileCache) Store(ctx context.Context, fingerprint string, content []byte) error {
filePath := fc.getFilePath(fingerprint)
if err := os.WriteFile(filePath, content, 0o600); err != nil {
return fmt.Errorf("failed to write cache file: %w", err)
}

return nil
}

// Clear removes all cached content from the cache directory.
func (fc *FileCache) Clear(ctx context.Context) error {
if _, err := os.Stat(fc.cachePath); os.IsNotExist(err) {
return nil
}

return os.RemoveAll(fc.cachePath)
}

// ClearFingerprint removes cached content for a specific fingerprint.
func (fc *FileCache) ClearFingerprint(ctx context.Context, fingerprint string) error {
filePath := fc.getFilePath(fingerprint)
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove cache file: %w", err)
}
return nil
}

// getFilePath returns the full file path for a given fingerprint.
func (fc *FileCache) getFilePath(fingerprint string) string {
return filepath.Join(fc.cachePath, fingerprint+".cache")
}

// GenerateFingerprint creates a SHA256 fingerprint from the provided data.
// This is a utility function for creating consistent fingerprints.
func GenerateFingerprint(data ...any) (string, error) {
hasher := sha256.New()

for _, item := range data {
var bytes []byte
var err error

switch v := item.(type) {
case string:
bytes = []byte(v)
case []byte:
bytes = v
case io.Reader:
bytes, err = io.ReadAll(v)
if err != nil {
return "", fmt.Errorf("failed to read data for fingerprint: %w", err)
}
default:
bytes, err = json.Marshal(v)
if err != nil {
return "", fmt.Errorf("failed to marshal data for fingerprint: %w", err)
}
}

hasher.Write(bytes)
}

hash := hasher.Sum(nil)
return hex.EncodeToString(hash[:16]), nil
}
Loading