Skip to content

Commit 0aef3ba

Browse files
authoredSep 1, 2024··
feat: improve fetch (#160)
* feat: improve fetch * chore: fetch should use hash
1 parent 0c5f986 commit 0aef3ba

File tree

15 files changed

+169
-73
lines changed

15 files changed

+169
-73
lines changed
 

‎alzlib.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"errors"
99
"fmt"
1010
"slices"
11-
"strconv"
1211
"strings"
1312
"sync"
1413

@@ -385,12 +384,12 @@ func (az *AlzLib) Init(ctx context.Context, libs ...LibraryReference) error {
385384
}
386385

387386
// Process the libraries
388-
for i, ref := range libs {
387+
for _, ref := range libs {
389388
if ref == nil {
390389
return errors.New("Alzlib.Init: library is nil")
391390
}
392391
if ref.FS() == nil {
393-
if _, err := ref.Fetch(ctx, strconv.Itoa(i)); err != nil {
392+
if _, err := ref.Fetch(ctx, hash(ref)); err != nil {
394393
return fmt.Errorf("Alzlib.Init: error fetching library %s: %w", ref, err)
395394
}
396395
}

‎cmd/alzlibtool/command/check/library.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ var libraryCmd = cobra.Command{
3232
}
3333
az.AddPolicyClient(cf)
3434
thisRef := alzlib.NewCustomLibraryReference(args[0])
35-
libs, err := alzlib.FetchLibraryWithDependencies(cmd.Context(), 0, thisRef, make(alzlib.LibraryReferences, 0, 5))
35+
libs, err := thisRef.FetchWithDependencies(cmd.Context())
3636
if err != nil {
3737
cmd.PrintErrf("%s could not fetch all libraries with dependencies: %v\n", cmd.ErrPrefix(), err)
3838
}

‎cmd/alzlibtool/command/document/library.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var documentLibraryBaseCmd = cobra.Command{
1515
Args: cobra.ExactArgs(1),
1616
Run: func(cmd *cobra.Command, args []string) {
1717
thislib := alzlib.NewCustomLibraryReference(args[0])
18-
alllibs, err := alzlib.FetchLibraryWithDependencies(cmd.Context(), 0, thislib, make(alzlib.LibraryReferences, 0, 5))
18+
alllibs, err := thislib.FetchWithDependencies(cmd.Context())
1919
if err != nil {
2020
cmd.PrintErrf("%s could not fetch all libraries with dependencies: %v\n", cmd.ErrPrefix(), err)
2121
}

‎initialize.go

+24-37
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ package alzlib
55

66
import (
77
"context"
8+
"crypto/sha256"
89
"fmt"
910
"io/fs"
1011
"net/url"
1112
"os"
1213
"path/filepath"
13-
"slices"
14-
"strconv"
1514

1615
"github.com/Azure/alzlib/internal/processor"
1716
"github.com/hashicorp/go-getter/v2"
@@ -24,46 +23,45 @@ const (
2423
alzLibraryGitUrlEnv = "ALZLIB_LIBRARY_GIT_URL" // alzLibraryGitUrlEnv is the environment variable to override the default git URL.
2524
)
2625

27-
// FetchLibraryWithDependencies takes a library reference, fetches it, and then fetches all of its dependencies.
26+
// fetchLibraryWithDependencies takes a library reference, fetches it, and then fetches all of its dependencies.
2827
// The destination directory is an integer that will be appended to the `.alzlib` directory in the current working directory.
2928
// This can be override by setting the `ALZLIB_DIR` environment variable.
30-
//
31-
// Example usage:
32-
//
33-
// ```go
34-
// az := alzlib.NewAlzLib(nil)
35-
// // ... ensure that clients are created and initialized
36-
// // e.g. az.AddPolicyClient(myClientFactory)
37-
// thisLib := NewCustomLibraryReference("path/to/library")
38-
// libs, err := FetchLibraryWithDependencies(ctx, ".alzlib", 0, thisLib, make(LibraryReferences, 0, 5))
39-
// // ... handle error
40-
//
41-
// err = az.Init(ctx, libs...)
42-
// // ... handle error
43-
// ```
44-
//
4529
// The `LibraryReferences` slice can be used to initialize the AlzLib instance.
46-
func FetchLibraryWithDependencies(ctx context.Context, i int, lib LibraryReference, libs LibraryReferences) (LibraryReferences, error) {
47-
f, err := lib.Fetch(ctx, strconv.Itoa(i))
30+
func fetchLibraryWithDependencies(ctx context.Context, processed map[string]bool, lib LibraryReference, result *LibraryReferences) error {
31+
if processed[lib.String()] {
32+
return nil
33+
}
34+
f, err := lib.Fetch(ctx, hash(lib))
4835
if err != nil {
49-
return nil, fmt.Errorf("FetchLibraryWithDependencies: error fetching library %s: %w", lib.String(), err)
36+
return fmt.Errorf("FetchLibraryWithDependencies: error fetching library %s: %w", lib.String(), err)
5037
}
5138
pscl := processor.NewProcessorClient(f)
5239
libmeta, err := pscl.Metadata()
5340
if err != nil {
54-
return nil, fmt.Errorf("FetchLibraryWithDependencies: error getting metadata for library %s: %w", lib.String(), err)
41+
return fmt.Errorf("FetchLibraryWithDependencies: error getting metadata for library %s: %w", lib.String(), err)
5542
}
5643
meta := NewMetadata(libmeta, lib)
5744
// for each dependency, recurse using this function
5845
for _, dep := range meta.Dependencies() {
59-
i++
60-
libs, err = FetchLibraryWithDependencies(ctx, i, dep, libs)
46+
err = fetchLibraryWithDependencies(ctx, processed, dep, result)
6147
if err != nil {
62-
return nil, fmt.Errorf("FetchLibraryWithDependencies: error fetching dependencies for library %s: %w", lib.String(), err)
48+
return fmt.Errorf("FetchLibraryWithDependencies: error fetching dependencies for library %s: %w", lib.String(), err)
6349
}
6450
}
6551
// add the current library reference to the list
66-
return addLibraryReferenceToSlice(libs, lib), nil
52+
*result = append(*result, lib)
53+
processed[lib.String()] = true
54+
return nil
55+
}
56+
57+
// hash returns the SHA224 hash of a fmt.Stringer, as a string.
58+
func hash(s fmt.Stringer) string {
59+
return hashStr(s.String())
60+
}
61+
62+
// hash returns the SHA224 hash of a string, as a string.
63+
func hashStr(s string) string {
64+
return fmt.Sprintf("%x", sha256.Sum224([]byte(s)))
6765
}
6866

6967
// FetchAzureLandingZonesLibraryByTag is a convenience function to fetch the Azure Landing Zones library by member path and tag (ref).
@@ -115,14 +113,3 @@ func FetchLibraryByGetterString(ctx context.Context, getterString, dstDir string
115113
}
116114
return os.DirFS(dst), nil
117115
}
118-
119-
// addLibraryReferenceToSlice adds a library reference to a slice if it does not already exist.
120-
func addLibraryReferenceToSlice(libs LibraryReferences, lib LibraryReference) LibraryReferences {
121-
if exists := slices.ContainsFunc(libs, func(l LibraryReference) bool {
122-
return l.String() == lib.String()
123-
}); exists {
124-
return libs
125-
}
126-
127-
return append(libs, lib)
128-
}

‎initialize_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func TestFetchLibraryWithDependencies(t *testing.T) {
2828
require.NoError(t, os.RemoveAll(".alzlib"))
2929
defer os.RemoveAll(".alzlib") // nolint: errcheck
3030

31-
libs, err := FetchLibraryWithDependencies(ctx, 0, NewCustomLibraryReference("./testdata/dependent-libs/lib1"), make(LibraryReferences, 0, 2))
31+
libs, err := NewCustomLibraryReference("./testdata/dependent-libs/lib1").FetchWithDependencies(ctx)
3232
assert.NoError(t, err)
3333
assert.Len(t, libs, 2)
3434
}
@@ -38,7 +38,7 @@ func TestFetchLibraryWithDependencies_MissingCustomDependency(t *testing.T) {
3838
require.NoError(t, os.RemoveAll(".alzlib"))
3939
defer os.RemoveAll(".alzlib") // nolint: errcheck
4040

41-
_, err := FetchLibraryWithDependencies(ctx, 0, NewCustomLibraryReference("./testdata/dependent-libs/missing-dep-custom"), make(LibraryReferences, 0, 2))
41+
_, err := NewCustomLibraryReference("./testdata/dependent-libs/missing-dep-custom").FetchWithDependencies(ctx)
4242
assert.ErrorContains(t, err, "could not fetch library member")
4343
}
4444

@@ -47,6 +47,6 @@ func TestFetchLibraryWithDependencies_MissingLibraryDependency(t *testing.T) {
4747
require.NoError(t, os.RemoveAll(".alzlib"))
4848
defer os.RemoveAll(".alzlib") // nolint: errcheck
4949

50-
_, err := FetchLibraryWithDependencies(ctx, 0, NewCustomLibraryReference("./testdata/dependent-libs/missing-dep-library"), make(LibraryReferences, 0, 2))
50+
_, err := NewCustomLibraryReference("./testdata/dependent-libs/missing-dep-library").FetchWithDependencies(ctx)
5151
assert.ErrorContains(t, err, "could not fetch library member")
5252
}

‎integrationtest/examples_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func Example_deploymentNewHierarchy() {
3232
az.AddPolicyClient(cf)
3333
//dirFs, err := alzlib.FetchAzureLandingZonesLibraryMember(ctx, alzLibraryMember, alzLibraryTag, "alz")
3434
lib := alzlib.NewCustomLibraryReference("testdata/alzlib-2024-07-01")
35-
libs, err := alzlib.FetchLibraryWithDependencies(ctx, 0, lib, make(alzlib.LibraryReferences, 0, 5))
35+
libs, err := lib.FetchWithDependencies(ctx)
3636
if err != nil {
3737
fmt.Println(err)
3838
return

‎internal/doc/doc.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,20 @@ func AlzlibReadmeMd(ctx context.Context, w io.Writer, libs ...alzlib.LibraryRefe
3636
}
3737

3838
func alzlibReadmeMdTitle(md *markdown.Markdown, metad *alzlib.Metadata) *markdown.Markdown {
39-
return md.H1f("%s (%s)", metad.Name(), metad.DisplayName()).LF().
40-
PlainText(metad.Description()).LF()
39+
name := metad.Name()
40+
if name == "" {
41+
name = "No name in metadata"
42+
}
43+
displayName := metad.DisplayName()
44+
if displayName == "" {
45+
displayName = "No display name in metadata"
46+
}
47+
description := metad.Description()
48+
if description == "" {
49+
description = "No description in metadata"
50+
}
51+
return md.H1f("%s (%s)", name, displayName).LF().
52+
PlainText(description).LF()
4153
}
4254

4355
func alzlibReadmeMdDependencies(md *markdown.Markdown, deps alzlib.LibraryReferences) *markdown.Markdown {

‎internal/processor/processor.go

+4-16
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ func (client *ProcessorClient) Metadata() (*LibMetadata, error) {
9393
var pe *fs.PathError
9494
if errors.As(err, &pe) {
9595
return &LibMetadata{
96-
Name: "No metadata file found",
97-
DisplayName: "No metadata file found",
98-
Description: "No metadata file found",
99-
Path: "No metadata file found",
96+
Name: "",
97+
DisplayName: "",
98+
Description: "",
99+
Path: "",
100100
Dependencies: make([]LibMetadataDependency, 0),
101101
}, nil
102102
}
@@ -114,18 +114,6 @@ func (client *ProcessorClient) Metadata() (*LibMetadata, error) {
114114
if err != nil {
115115
return nil, fmt.Errorf("ProcessorClient.Metadata: error unmarshaling metadata: %w", err)
116116
}
117-
if metadata.Description == "" {
118-
metadata.Description = "No description provided"
119-
}
120-
if metadata.DisplayName == "" {
121-
metadata.DisplayName = "No display name provided"
122-
}
123-
if metadata.Path == "" {
124-
metadata.Path = "No path provided"
125-
}
126-
if metadata.Name == "" {
127-
metadata.Name = "No name provided"
128-
}
129117
for _, dep := range metadata.Dependencies {
130118
switch {
131119
case dep.Path != "" && dep.Ref != "" && dep.CustomUrl == "":

‎metadata.go

+34-3
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,34 @@ type Metadata struct {
2727
type LibraryReferences []LibraryReference
2828

2929
// FSs returns the filesystems of the library references, can be used with Alzlib.Init().
30-
func (lr LibraryReferences) FSs() []fs.FS {
31-
fss := make([]fs.FS, len(lr))
32-
for i, l := range lr {
30+
func (m LibraryReferences) FSs() []fs.FS {
31+
fss := make([]fs.FS, len(m))
32+
for i, l := range m {
3333
fss[i] = l.FS()
3434
}
3535
return fss
3636
}
3737

38+
// FetchWithDependencies recursively fetches all the library references and their dependencies.
39+
// The destination directory a hash value that will be appended to the `.alzlib` directory in the current working directory unless overridden by the `ALZLIB_DIR` environment variable.
40+
func (m LibraryReferences) FetchWithDependencies(ctx context.Context) (LibraryReferences, error) {
41+
processed := make(map[string]bool)
42+
result := make(LibraryReferences, 0, 5)
43+
for _, lib := range m {
44+
err := fetchLibraryWithDependencies(ctx, processed, lib, &result)
45+
if err != nil {
46+
return nil, err
47+
}
48+
}
49+
return result, nil
50+
}
51+
3852
// LibraryReference is an interface that represents a dependency of a library member.
3953
// It can be fetched form either a custom go-getter URL or from the ALZ Library.
4054
type LibraryReference interface {
4155
fmt.Stringer
4256
Fetch(ctx context.Context, desinationDirectory string) (fs.FS, error) // Fetch fetches the library member to the `.alzlib/destinationDirectory`. Override the base dir using `ALZLIB_DIR` env var.
57+
FetchWithDependencies(ctx context.Context) (LibraryReferences, error) // FetchWithDependencies fetches the library member and its dependencies.
4358
FS() fs.FS // FS returns the filesystem of the library member, can be used in Alzlib.Init()
4459
}
4560

@@ -92,6 +107,14 @@ func (m *AlzLibraryReference) Ref() string {
92107
return m.ref
93108
}
94109

110+
// FetchWithDependencies fetches the library member and its dependencies.
111+
// If you have more than one LibraryReference in a LibraryReferences slice, use LibraryReferences.FetchWithDependencies() instead.
112+
func (m *AlzLibraryReference) FetchWithDependencies(ctx context.Context) (LibraryReferences, error) {
113+
processed := make(map[string]bool)
114+
result := make(LibraryReferences, 0, 5)
115+
return result, fetchLibraryWithDependencies(ctx, processed, m, &result)
116+
}
117+
95118
// CustomLibraryReference is a struct that represents a dependency of a library member that is fetched from a custom go-getter URL.
96119
type CustomLibraryReference struct {
97120
url string
@@ -128,6 +151,14 @@ func (m *CustomLibraryReference) String() string {
128151
return m.url
129152
}
130153

154+
// FetchWithDependencies fetches the library member and its dependencies.
155+
// If you have more than one LibraryReference in a LibraryReferences slice, use LibraryReferences.FetchWithDependencies() instead.
156+
func (m *CustomLibraryReference) FetchWithDependencies(ctx context.Context) (LibraryReferences, error) {
157+
processed := make(map[string]bool)
158+
result := make(LibraryReferences, 0, 5)
159+
return result, fetchLibraryWithDependencies(ctx, processed, m, &result)
160+
}
161+
131162
func NewMetadata(in *processor.LibMetadata, ref LibraryReference) *Metadata {
132163
dependencies := make([]LibraryReference, len(in.Dependencies))
133164
for i, dep := range in.Dependencies {

‎metadata_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package alzlib
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestFetchLibrariesWithDependencies tests fetching libraries with dependencies and they they are fetched in the right order.
13+
func TestFetchLibrariesWithDependencies(t *testing.T) {
14+
ctx := context.Background()
15+
require.NoError(t, os.RemoveAll(".alzlib"))
16+
defer os.RemoveAll(".alzlib") // nolint: errcheck
17+
expcted := []string{
18+
"testdata/dependent-libs/lib2",
19+
"testdata/dependent-libs/lib1",
20+
"testdata/dependent-libs/libB",
21+
"testdata/dependent-libs/libA",
22+
}
23+
lib1 := NewCustomLibraryReference("testdata/dependent-libs/lib1")
24+
libA := NewCustomLibraryReference("testdata/dependent-libs/libA")
25+
libs := LibraryReferences{lib1, libA}
26+
libs, err := libs.FetchWithDependencies(ctx)
27+
assert.NoError(t, err)
28+
require.Len(t, libs, 4)
29+
result := make([]string, 4)
30+
for i, lib := range libs {
31+
result[i] = lib.String()
32+
}
33+
assert.ElementsMatch(t, expcted, result)
34+
}
35+
36+
// TestFetchLibrariesWithCommonDependency checks that a libraries having a common dependency is fetched only once.
37+
func TestFetchLibrariesWithCommonDependency(t *testing.T) {
38+
ctx := context.Background()
39+
require.NoError(t, os.RemoveAll(".alzlib"))
40+
defer os.RemoveAll(".alzlib") // nolint: errcheck
41+
expcted := []string{
42+
"testdata/dependent-libs/lib2",
43+
"testdata/dependent-libs/lib1",
44+
"testdata/dependent-libs/lib3",
45+
}
46+
lib1 := NewCustomLibraryReference("testdata/dependent-libs/lib1")
47+
libA := NewCustomLibraryReference("testdata/dependent-libs/lib3")
48+
libs := LibraryReferences{lib1, libA}
49+
libs, err := libs.FetchWithDependencies(ctx)
50+
assert.NoError(t, err)
51+
require.Len(t, libs, 3)
52+
result := make([]string, 3)
53+
for i, lib := range libs {
54+
result[i] = lib.String()
55+
}
56+
assert.ElementsMatch(t, expcted, result)
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/Azure/Azure-Landing-Zones-Library/main/schemas/library_metadata.json",
3+
"name": "lib3",
4+
"display_name": "library 3",
5+
"description": "Like lib1, lib 3 is dependent on lib 2",
6+
"dependencies": [
7+
{
8+
"custom_url": "testdata/dependent-libs/lib2"
9+
}
10+
]
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/Azure/Azure-Landing-Zones-Library/main/schemas/library_metadata.json",
3+
"name": "libA",
4+
"display_name": "library A",
5+
"description": "libA is dependent on libB",
6+
"dependencies": [
7+
{
8+
"custom_url": "testdata/dependent-libs/libB"
9+
}
10+
]
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/Azure/Azure-Landing-Zones-Library/main/schemas/library_metadata.json",
3+
"name": "libB",
4+
"display_name": "library B",
5+
"description": "library B description"
6+
}

‎to/doc.go

-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
//go:build go1.18
2-
// +build go1.18
3-
41
// Copyright 2017 Microsoft Corporation. All rights reserved.
52
// Use of this source code is governed by an MIT
63
// license that can be found in the LICENSE file.

0 commit comments

Comments
 (0)
Please sign in to comment.