Skip to content

Commit 0812f24

Browse files
authored
Merge pull request #6 from permafrost-dev/add-cache
implement cache
2 parents b5ef792 + d8233fb commit 0812f24

File tree

6 files changed

+410
-53
lines changed

6 files changed

+410
-53
lines changed

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ The `settings` section of the configuration file is used to configure the applic
5757
| `defaults.tasks.platforms` | default platforms for tasks | no |
5858
| `defaults.tasks.silent` | default silent setting for tasks | no |
5959
| `dotenv` | array of `.env` filenames to load | no |
60+
| `cache.ttl-minutes` | number of minutes to cache remote files | no |
6061
| `exit-on-checksum-mismatch` | `boolean` value specifying whether to exit if a checksum mismatch occurs when including a remote file | no |
6162

6263
Example `settings` section:
@@ -68,6 +69,8 @@ version: 1.0.0
6869
settings:
6970
dotenv: ['.env', '.env.local'] # loads both `.env` and `.env.local` files, defaults to `.env`.
7071
exit-on-checksum-mismatch: false # do not exit if a checksum mismatch occurs, defaults to true.
72+
cache:
73+
ttl-minutes: 60 # cache remote files for 60 minutes, defaults to 5 minutes.
7174
defaults:
7275
tasks:
7376
silent: true
@@ -102,8 +105,19 @@ env:
102105
The `includes` section of the configuration file is used to specify a list of filenames, file urls, or s3 urls that should be merged with the configuration. This is useful for splitting up a large configuration file into smaller, more manageable files or reusing commonly-used tasks, init scripts, or preconditions. Startup, shutdown, servers, and scheduled tasks are not merged from the included files.
103106

104107
Included urls can be prefixed with `gh:` to indicate that the file should be fetched from GitHub. For example, `gh:permafrost-dev/stackup/main/templates/stackup.dist.yaml` will fetch the `stackup.dist.yaml` file from the `permafrost-dev/stackup` repository on GitHub.
108+
Add a `headers` field to the `url` entry to specify headers to send with the request. The `headers` field should be an array of strings, where each string is a header to send with the request. The header value can be a javascript expression if wrapped in double braces. For example:
105109

106-
To use a file from an S3 bucket, prefix the url with `s3:`. For example, `s3:hostname/my-bucket-name/my-config.yaml` will fetch the `my-config.yaml` file from the `my-bucket-name` bucket on `hostname`. Amazon S3 and Minio are supported.
110+
```yaml
111+
- url: gh:permafrost-dev/stackup/main/templates/remote-includes/node.yaml
112+
headers:
113+
- 'Authorization: token $GITHUB_TOKEN'
114+
115+
- url: gh:permafrost-dev/stackup/main/templates/remote-includes/php.yaml
116+
headers:
117+
- '{{ "Authorization: token " + $myGithubTokenVar }}'
118+
```
119+
120+
To import a file from an S3 bucket, prefix the url with `s3:`. For example, `s3:hostname/my-bucket-name/my-config.yaml` will fetch the `my-config.yaml` file from the `my-bucket-name` bucket on `hostname`. Amazon S3 and Minio are supported.
107121

108122
Included files can be specified with either a relative or absolute pathname. Relative pathnames are relative to the directory containing the configuration file. Absolute pathnames are relative to the current working directory.
109123

@@ -112,6 +126,10 @@ includes:
112126
- url: gh:permafrost-dev/stackup/main/templates/remote-includes/containers.yaml
113127
verify: false # optional, defaults to true
114128
129+
- url: gh:permafrost-dev/stackup/main/templates/remote-includes/node.yaml
130+
headers:
131+
- 'Authorization: token $GITHUB_TOKEN' # headers to send with the request, can be javascript if wrapped in double braces
132+
115133
- file: python.yaml # includes a local file
116134
117135
- url: s3:127.0.0.1:9000/stackup-includes/python.yaml # includes a file from a minio bucket

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ require (
4242
require (
4343
github.com/AlecAivazis/survey/v2 v2.3.7
4444
github.com/emirpasic/gods v1.18.1
45+
github.com/golang-module/carbon/v2 v2.2.3
4546
github.com/kr/pretty v0.3.1 // indirect
4647
github.com/minio/minio-go v6.0.14+incompatible
4748
github.com/minio/minio-go/v7 v7.0.61
49+
go.etcd.io/bbolt v1.3.7
4850
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
4951
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJ
1717
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
1818
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
1919
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
20+
github.com/golang-module/carbon/v2 v2.2.3 h1:WvGIc5+qzq9drNzH+Gnjh1TZ0JgDY/IA+m2Dvk7Qm4Q=
21+
github.com/golang-module/carbon/v2 v2.2.3/go.mod h1:LdzRApgmDT/wt0eNT8MEJbHfJdSqCtT46uZhfF30dqI=
2022
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
2123
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
2224
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -79,12 +81,19 @@ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
7981
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
8082
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
8183
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
84+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
85+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
8286
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
8387
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8488
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
89+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
90+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
91+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
8592
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
8693
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
8794
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
95+
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
96+
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
8897
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
8998
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
9099
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
@@ -134,3 +143,4 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
134143
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
135144
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
136145
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
146+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

lib/app/workflow.go

Lines changed: 118 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55
"os"
66
"regexp"
77
"strings"
8+
"sync"
89

910
lla "github.com/emirpasic/gods/lists/arraylist"
1011
lls "github.com/emirpasic/gods/stacks/linkedliststack"
12+
"github.com/stackup-app/stackup/lib/cache"
1113
"github.com/stackup-app/stackup/lib/checksums"
1214
"github.com/stackup-app/stackup/lib/downloader"
1315
"github.com/stackup-app/stackup/lib/support"
@@ -31,26 +33,33 @@ type StackupWorkflow struct {
3133
Scheduler []ScheduledTask `yaml:"scheduler"`
3234
Includes []*WorkflowInclude `yaml:"includes"`
3335
State *StackupWorkflowState
36+
Cache *cache.Cache
3437
}
3538

3639
type WorkflowInclude struct {
37-
Url string `yaml:"url"`
38-
File string `yaml:"file"`
39-
ChecksumUrl string `yaml:"checksum-url"`
40-
VerifyChecksum *bool `yaml:"verify,omitempty"`
41-
AccessKey string `yaml:"access-key"`
42-
SecretKey string `yaml:"secret-key"`
43-
Secure bool `yaml:"secure"`
40+
Url string `yaml:"url"`
41+
Headers []string `yaml:"headers"`
42+
File string `yaml:"file"`
43+
ChecksumUrl string `yaml:"checksum-url"`
44+
VerifyChecksum *bool `yaml:"verify,omitempty"`
45+
AccessKey string `yaml:"access-key"`
46+
SecretKey string `yaml:"secret-key"`
47+
Secure bool `yaml:"secure"`
4448
ChecksumIsValid *bool
4549
ValidationState string
4650
Contents string
51+
Hash string
52+
Workflow *StackupWorkflow
4753
}
4854

4955
type WorkflowSettings struct {
5056
Defaults *WorkflowSettingsDefaults `yaml:"defaults"`
5157
ExitOnChecksumMismatch bool `yaml:"exit-on-checksum-mismatch"`
5258
DotEnvFiles []string `yaml:"dotenv"`
53-
Domains struct {
59+
Cache struct {
60+
TtlMinutes int `yaml:"ttl-minutes"`
61+
} `yaml:"cache"`
62+
Domains struct {
5463
Allowed []string `yaml:"allowed"`
5564
} `yaml:"domains"`
5665
}
@@ -141,32 +150,39 @@ func (wi *WorkflowInclude) ValidateChecksum(contents string) (bool, error) {
141150

142151
algorithm := ""
143152
storedChecksum := ""
153+
checksumContents := ""
154+
hashUrl := ""
144155

145156
for _, url := range checksumUrls {
157+
if wi.Workflow.Cache.Has(url) && !wi.Workflow.Cache.IsExpired(url) {
158+
hashUrl = url
159+
checksumContents = wi.Workflow.Cache.Get(url)
160+
fmt.Printf("using cached checksum file %s\n", url)
161+
break
162+
}
163+
146164
checksumContents, err := utils.GetUrlContents(url)
147165
if err != nil {
148-
fmt.Printf("error: %s\n", err)
149166
continue
150167
}
151168

152-
// fmt.Printf("using checksum file %s\n", url)
153-
154169
if checksumContents != "" {
155-
storedChecksum = wi.getChecksumFromContents(checksumContents)
156-
157-
wi.ChecksumUrl = url
158-
if strings.HasSuffix(url, ".sha256") || strings.HasSuffix(url, ".sha256.txt") {
159-
algorithm = "sha256"
160-
}
161-
if strings.HasSuffix(url, ".sha512") || strings.HasSuffix(url, ".sha512.txt") {
162-
algorithm = "sha512"
163-
}
170+
hashUrl = url
171+
wi.Workflow.Cache.Set(url, checksumContents, wi.Workflow.Settings.Cache.TtlMinutes)
172+
fmt.Printf("using non-cached checksum file %s\n", url)
164173
break
165174
}
166175
}
167176

168-
if algorithm == "" {
169-
// return false, fmt.Errorf("unable to find valid checksum file for %s", wi.DisplayUrl())
177+
if checksumContents != "" {
178+
storedChecksum = wi.getChecksumFromContents(checksumContents)
179+
180+
wi.ChecksumUrl = hashUrl
181+
algorithm = wi.GetChecksumAlgorithm()
182+
}
183+
184+
if algorithm == "unknown" {
185+
return false, fmt.Errorf("unable to find valid checksum file for %s", wi.DisplayUrl())
170186
}
171187

172188
var hash string
@@ -183,11 +199,6 @@ func (wi *WorkflowInclude) ValidateChecksum(contents string) (bool, error) {
183199
return false, fmt.Errorf("unsupported algorithm: %s", algorithm)
184200
}
185201

186-
// fmt.Printf("checksum url: %s\n", wi.ChecksumUrl)
187-
// fmt.Printf("algorithm: %s\n", algorithm)
188-
// fmt.Printf("hash: %s\n", hash)
189-
// fmt.Printf("checksum: %s\n", storedChecksum)
190-
191202
if !strings.EqualFold(hash, storedChecksum) {
192203
wi.SetChecksumIsValid(false)
193204
return false, nil
@@ -252,6 +263,17 @@ func (wi *WorkflowInclude) DisplayName() string {
252263
return "<unknown>"
253264
}
254265

266+
func (wi *WorkflowInclude) GetChecksumAlgorithm() string {
267+
if strings.HasSuffix(wi.ChecksumUrl, ".sha256") || strings.HasSuffix(wi.ChecksumUrl, ".sha256.txt") {
268+
return "sha256"
269+
}
270+
if strings.HasSuffix(wi.ChecksumUrl, ".sha512") || strings.HasSuffix(wi.ChecksumUrl, ".sha512.txt") {
271+
return "sha512"
272+
}
273+
274+
return "unknown"
275+
}
276+
255277
func (wi *WorkflowInclude) SetChecksumIsValid(value bool) {
256278
wi.ChecksumIsValid = &value
257279
}
@@ -296,6 +318,8 @@ func (workflow *StackupWorkflow) reversePreconditions(items []*Precondition) []*
296318
}
297319

298320
func (workflow *StackupWorkflow) Initialize() {
321+
workflow.Cache = cache.CreateCache(utils.GetProjectName())
322+
299323
// generate uuids for each task as the initial step, as other code below relies on a uuid existing
300324
for _, task := range workflow.Tasks {
301325
task.Uuid = utils.GenerateTaskUuid()
@@ -322,6 +346,10 @@ func (workflow *StackupWorkflow) Initialize() {
322346
}
323347
}
324348

349+
if workflow.Settings.Cache.TtlMinutes <= 0 {
350+
workflow.Settings.Cache.TtlMinutes = 5
351+
}
352+
325353
if len(workflow.Settings.DotEnvFiles) == 0 {
326354
workflow.Settings.DotEnvFiles = []string{".env"}
327355
}
@@ -341,6 +369,11 @@ func (workflow *StackupWorkflow) Initialize() {
341369
}
342370
}
343371

372+
// initialize the includes
373+
for _, inc := range workflow.Includes {
374+
inc.Initialize(workflow)
375+
}
376+
344377
workflow.ProcessIncludes()
345378

346379
if len(workflow.Init) > 0 {
@@ -373,20 +406,22 @@ func (workflow *StackupWorkflow) RemoveTasks(uuidsToRemove []string) {
373406
workflow.Tasks = newTasks
374407
}
375408

376-
func (workflow *StackupWorkflow) ProcessIncludes() {
377-
// set default value for verify checksum to true
378-
for _, wi := range workflow.Includes {
379-
if wi.VerifyChecksum == nil {
380-
boolValue := true //wi.ChecksumUrl != ""
381-
wi.VerifyChecksum = &boolValue
382-
}
383-
wi.ValidationState = "not validated"
384-
wi.ChecksumIsValid = nil
385-
}
409+
// func (workflow *StackupWorkflow) ProcessIncludes() {
410+
// for _, include := range workflow.Includes {
411+
// workflow.ProcessInclude(include)
412+
// }
413+
// }
386414

415+
func (workflow *StackupWorkflow) ProcessIncludes() {
416+
var wg sync.WaitGroup
387417
for _, include := range workflow.Includes {
388-
workflow.ProcessInclude(include)
418+
wg.Add(1)
419+
go func(include *WorkflowInclude) {
420+
defer wg.Done()
421+
workflow.ProcessInclude(include)
422+
}(include)
389423
}
424+
wg.Wait()
390425
}
391426

392427
func (workflow *StackupWorkflow) ProcessInclude(include *WorkflowInclude) bool {
@@ -397,22 +432,33 @@ func (workflow *StackupWorkflow) ProcessInclude(include *WorkflowInclude) bool {
397432
var contents string
398433
var err error
399434

400-
if include.IsLocalFile() {
401-
contents, err = utils.GetFileContents(include.Filename())
402-
} else if include.IsRemoteUrl() {
403-
contents, err = utils.GetUrlContents(include.FullUrl())
404-
} else if include.IsS3Url() {
405-
include.AccessKey = os.ExpandEnv(include.AccessKey)
406-
include.SecretKey = os.ExpandEnv(include.SecretKey)
435+
if workflow.Cache.Has(include.DisplayName()) && !workflow.Cache.IsExpired(include.DisplayName()) {
436+
include.Contents = workflow.Cache.Get(include.DisplayName())
437+
include.Hash = workflow.Cache.GetHash(include.DisplayName())
438+
fmt.Println("loaded from cache")
439+
}
440+
441+
if !workflow.Cache.Has(include.DisplayName()) || workflow.Cache.IsExpired(include.DisplayName()) {
442+
fmt.Println("not loaded from cache")
443+
if include.IsLocalFile() {
444+
include.Contents, err = utils.GetFileContents(include.Filename())
445+
} else if include.IsRemoteUrl() {
446+
include.Contents, err = utils.GetUrlContentsEx(include.FullUrl(), include.Headers)
447+
} else if include.IsS3Url() {
448+
include.AccessKey = os.ExpandEnv(include.AccessKey)
449+
include.SecretKey = os.ExpandEnv(include.SecretKey)
450+
include.Contents = downloader.ReadS3FileContents(include.FullUrl(), include.AccessKey, include.SecretKey, include.Secure)
451+
} else {
452+
return false
453+
}
407454

408-
contents = downloader.ReadS3FileContents(include.FullUrl(), include.AccessKey, include.SecretKey, include.Secure)
409-
} else {
410-
return false
455+
include.Hash = checksums.CalculateSha256Hash(include.Contents)
456+
workflow.Cache.Set(include.DisplayName(), include.Contents, workflow.Settings.Cache.TtlMinutes)
411457
}
412458

413-
contents = strings.TrimSpace(contents)
414-
415-
include.Contents = contents
459+
// fmt.Printf("value: %v\n", workflow.Cache.Get(include.DisplayName()))
460+
// fmt.Printf("expires: %v\n", workflow.Cache.GetExpiresAt(include.DisplayName()))
461+
// fmt.Printf("hash: %v\n", workflow.Cache.GetHash(include.DisplayName()))
416462

417463
// fmt.Printf("include: %s\n", include.DisplayName())
418464
// fmt.Printf("include: %s\n", include.FullUrl())
@@ -428,7 +474,7 @@ func (workflow *StackupWorkflow) ProcessInclude(include *WorkflowInclude) bool {
428474
if include.IsRemoteUrl() {
429475
if *include.VerifyChecksum == true || include.VerifyChecksum == nil {
430476
//support.StatusMessage("Validating checksum for remote include: "+include.DisplayUrl(), false)
431-
validated, err := include.ValidateChecksum(contents)
477+
validated, err := include.ValidateChecksum(include.Contents)
432478

433479
if include.ChecksumIsValid != nil && *include.ChecksumIsValid == true {
434480
include.ValidationState = "checksum validated"
@@ -488,3 +534,23 @@ func (workflow *StackupWorkflow) ProcessInclude(include *WorkflowInclude) bool {
488534

489535
return true
490536
}
537+
538+
func (wi *WorkflowInclude) Initialize(workflow *StackupWorkflow) {
539+
wi.Workflow = workflow
540+
541+
// expand environment variables in the include headers
542+
for i, v := range wi.Headers {
543+
if App.JsEngine.IsEvaluatableScriptString(v) {
544+
wi.Headers[i] = App.JsEngine.Evaluate(v).(string)
545+
}
546+
wi.Headers[i] = os.ExpandEnv(v)
547+
}
548+
549+
// set some default values
550+
if wi.VerifyChecksum == nil {
551+
boolValue := true //wi.ChecksumUrl != ""
552+
wi.VerifyChecksum = &boolValue
553+
}
554+
wi.ValidationState = "not validated"
555+
wi.ChecksumIsValid = nil
556+
}

0 commit comments

Comments
 (0)