├── .gitignore ├── README.md ├── build.sh ├── buildpacks ├── create_buildpackage.go ├── create_buildpackage_test.go ├── new_version.go ├── rewrite_layer.go └── update_buildpack.go ├── cmd └── kpdemo │ └── main.go ├── defaults └── demo.go ├── docs └── assets │ └── sample.png ├── go.mod ├── go.sum ├── images └── current.go ├── k8s └── config.go ├── logs └── logs.go ├── populate ├── cleanup.go ├── populate.go └── relocate.go ├── rebase └── update_run_image.go ├── server ├── open_browser.go └── server.go ├── setup └── minikube │ ├── README.md │ ├── docker-compose.yml │ └── service.yaml └── ui ├── .gitignore ├── package-lock.json ├── package.json ├── public ├── index.html └── robots.txt └── src ├── App.css ├── App.js ├── AppInfo.js ├── Modal.js ├── images.js ├── index.css ├── index.js ├── logo.svg └── serviceWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | /statik 15 | /kpdemo 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kpdemo 2 | 3 | A tool to visualize and demo [kpack](https://github.com/pivotal/kpack). 4 | 5 | ![Sample](docs/assets/sample.png) 6 | 7 | #### Prerequisites 8 | 9 | - Access to a kubernetes cluster with [kpack installed](https://github.com/pivotal/kpack/releases). 10 | - Cluster-admin permissions for the kubernetes cluster with kpack. 11 | - Accessible Docker V2 Registry. 12 | 13 | ## Get Started 14 | 15 | 1. Download the newest [release](https://github.com/matthewmcnew/kpdemo/releases) 16 | 2. Run `kpdemo serve` to get a visualization of the images inside of a kpack cluster. 17 | 18 | ### Demos 19 | 20 | 1. Start the local server for the kpack visualization web UI 21 | 22 | ```bash 23 | kpdemo serve 24 | ``` 25 | 26 | > This should start up a local kpack visualization web server that you access in the browser. 27 | 28 | 1. Populate kpack with sample image configurations. 29 | 30 | The `kpdemo populate` command will relocate builder and run images to a configured registry to enable kpack demos. 31 | In addition, the command will "seed" a specified number of sample kpack image configurations. 32 | 33 | Running `kpdemo populate` will look something like this: 34 | ```bash 35 | kpdemo populate --registry gcr.io/my-project-name --count 20 36 | ``` 37 | 38 | - `registry`: The registry to install kpack images & for kpack to build new images into. You need local write access to this registry. 39 | 40 | - `count`: The number of initial kpack image configurations to create. 41 | 42 | - (Optional) `cache-size`: The Cache Size for each image's build cache. Example: `--cache-size 100Mi` Default: '500Mi' 43 | 44 | > Warning: The registry configured in kpdemo populate must be publicly readable by kpack. 45 | 46 | 1. Navigate to the Web UI in your browser to see kpack build all the images created in step #3. 47 | 48 | ## Demo: Stack Update 49 | 50 | 1. Navigate to the kpack web UI and mark the current stack (run image) as 'vulnerable'. 51 | 52 | - Copy the truncated stack digest from from one of the existing images in the visualization. 53 | - Click on Setup in the top right corner. 54 | - Paste the stack (run image) Digest into the Modal. 55 | - Click Save. 56 | - You should see the images with that run image highlighted in red. 57 | 58 | 1. Push an updated stack (Run Image) 59 | 60 | The `kpdemo update-stack` will push an updated image to the registry kpack is monitoring. 61 | 62 | ``` 63 | kpdemo update-stack 64 | ``` 65 | 66 | 1. Navigate to the Web UI in your browser to watch kpack `rebase` all the images that used the previous stack (run image). 67 | 68 | ## Demo: Buildpack update 69 | 70 | 1. Navigate to the kpack web UI and mark a buildpack id & version as 'vulnerable'. 71 | 72 | - Copy the current backpack ID & Version for from one of the existing images in the visualization. 73 | - Click on Setup in the top right corner. 74 | - Paste the Buildpack ID & Version into the Modal. 75 | - Click Save. 76 | - You should see the images that were built with that buildpack highlighted in red. 77 | 78 | 1. Push an Updated Backpack 79 | 80 | The `kpdemo update-buildpack --buildpack ` will create a new buildpack and add it to the kpack buildpack store. Kpack will rebuild "out-of-date" images with the new buildpack. 81 | 82 | ``` 83 | kpdemo update-buildpack --buildpack 84 | ``` 85 | 86 | 87 | 1. Navigate to the Web UI in your browser to watch kpack `rebuild` all the images that used the previous buildpack. 88 | 89 | 90 | ## Image logs 91 | 92 | You can view the build logs of any image in any namespace `kpdemo `. 93 | 94 | ``` 95 | kpdemo logs 96 | ``` 97 | 98 | ## Cleanup 99 | 100 | 1. Remove all images created by `kpdemo` with `cleanup` 101 | 102 | ``` 103 | kpdemo cleanup 104 | ``` 105 | 106 | Note: this will reset your kpack builder,stack, and store resources to their previous state before using kpdemo. 107 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | pushd ui 2 | npm install 3 | npm run-script build 4 | popd 5 | 6 | statik -src=ui/build 7 | 8 | go build -o kpdemo cmd/kpdemo/main.go 9 | -------------------------------------------------------------------------------- /buildpacks/create_buildpackage.go: -------------------------------------------------------------------------------- 1 | package buildpacks 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/google/go-containerregistry/pkg/authn" 7 | "github.com/google/go-containerregistry/pkg/name" 8 | v1 "github.com/google/go-containerregistry/pkg/v1" 9 | "github.com/google/go-containerregistry/pkg/v1/mutate" 10 | "github.com/google/go-containerregistry/pkg/v1/random" 11 | "github.com/google/go-containerregistry/pkg/v1/remote" 12 | "github.com/pivotal/kpack/pkg/registry/imagehelpers" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | func buildpackage(id, version, source, destination string) (string, error) { 17 | reference, err := name.ParseReference(source) 18 | if err != nil { 19 | return "", err 20 | } 21 | 22 | sourceImage, err := remote.Image(reference, remote.WithAuthFromKeychain(authn.DefaultKeychain)) 23 | if err != nil { 24 | return "", err 25 | } 26 | 27 | metadata := BuildpackLayerMetadata{} 28 | err = imagehelpers.GetLabel(sourceImage, "io.buildpacks.buildpack.layers", &metadata) 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | info, layers, err := metadata.metadataAndLayersFor(BuildpackLayerMetadata{}, sourceImage, id, version) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | newBuildpackage, err := random.Image(0, 0) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | newBuildpackage, err = mutate.AppendLayers(newBuildpackage, layers...) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | newVersion, err := newVersion(id, version) 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | newBuildpackage, err = imagehelpers.SetLabels(newBuildpackage, map[string]interface{}{ 54 | "io.buildpacks.buildpack.layers": info, 55 | "io.buildpacks.buildpackage.metadata": Metadata{ 56 | BuildpackInfo: BuildpackInfo{ 57 | Id: id, 58 | Version: newVersion, 59 | }, 60 | }, 61 | }) 62 | if err != nil { 63 | return "", err 64 | } 65 | 66 | reference, err = name.ParseReference(destination) 67 | if err != nil { 68 | return "", err 69 | } 70 | 71 | err = remote.Write(reference, newBuildpackage, remote.WithAuthFromKeychain(authn.DefaultKeychain)) 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | digest, err := newBuildpackage.Digest() 77 | if err != nil { 78 | return "", err 79 | } 80 | 81 | identifer := fmt.Sprintf("%s@%s", destination, digest.String()) 82 | 83 | return identifer, nil 84 | } 85 | 86 | type BuildpackLayerMetadata map[string]map[string]BuildpackLayerInfo 87 | 88 | func (m BuildpackLayerMetadata) metadataAndLayersFor(initalMetadata BuildpackLayerMetadata, sourceImage v1.Image, id string, version string) (BuildpackLayerMetadata, []v1.Layer, error) { 89 | bps, ok := m[id] 90 | if !ok { 91 | return m, nil, errors.Errorf("could not find %s", id) 92 | } 93 | 94 | info, ok := bps[version] 95 | if !ok { 96 | return m, nil, errors.Errorf("could not find %s@%s", id, version) 97 | } 98 | 99 | var layers []v1.Layer 100 | var newOrder Order 101 | for _, oe := range info.Order { 102 | var newGroup []BuildpackRef 103 | for _, g := range oe.Group { 104 | var err error 105 | var ls []v1.Layer 106 | 107 | initalMetadata, ls, err = m.metadataAndLayersFor(initalMetadata, sourceImage, g.Id, g.Version) 108 | if err != nil { 109 | return m, nil, err 110 | } 111 | layers = append(layers, ls...) 112 | 113 | newVersion, err := newVersion(g.Id, g.Version) 114 | if err != nil { 115 | return m, nil, err 116 | } 117 | 118 | newGroup = append(newGroup, BuildpackRef{ 119 | BuildpackInfo: BuildpackInfo{ 120 | Id: g.Id, 121 | Version: newVersion, 122 | }, 123 | Optional: g.Optional, 124 | }) 125 | } 126 | newOrder = append(newOrder, OrderEntry{Group: newGroup}) 127 | } 128 | 129 | hash, err := v1.NewHash(info.LayerDiffID) 130 | if err != nil { 131 | return m, nil, err 132 | } 133 | 134 | bpl, err := sourceImage.LayerByDiffID(hash) 135 | if err != nil { 136 | return m, nil, err 137 | } 138 | 139 | newVersion, err := newVersion(id, version) 140 | if err != nil { 141 | return m, nil, err 142 | } 143 | newBpL, err := rewriteLayer(bpl, version, newVersion) 144 | if err != nil { 145 | return m, nil, err 146 | } 147 | 148 | diffID, err := newBpL.DiffID() 149 | if err != nil { 150 | return m, nil, err 151 | } 152 | 153 | _, ok = initalMetadata[id] 154 | if !ok { 155 | initalMetadata[id] = map[string]BuildpackLayerInfo{} 156 | } 157 | initalMetadata[id][newVersion] = info 158 | initalMetadata[id][newVersion] = BuildpackLayerInfo{ 159 | API: info.API, 160 | Stacks: info.Stacks, 161 | Order: newOrder, 162 | LayerDiffID: diffID.String(), 163 | } 164 | 165 | return initalMetadata, append(layers, newBpL), nil 166 | } 167 | 168 | type BuildpackLayerInfo struct { 169 | API string `json:"api"` 170 | Stacks []Stack `json:"stacks,omitempty"` 171 | Order Order `json:"order,omitempty"` 172 | LayerDiffID string `json:"layerDiffID"` 173 | } 174 | 175 | type Order []OrderEntry 176 | 177 | type OrderEntry struct { 178 | Group []BuildpackRef `json:"group,omitempty"` 179 | } 180 | 181 | type BuildpackRef struct { 182 | BuildpackInfo `json:",inline"` 183 | Optional bool `json:"optional,omitempty"` 184 | } 185 | 186 | type BuildpackInfo struct { 187 | Id string `json:"id"` 188 | Version string `json:"version,omitempty"` 189 | } 190 | 191 | type Stack struct { 192 | ID string `json:"id"` 193 | Mixins []string `json:"mixins,omitempty"` 194 | } 195 | 196 | type Metadata struct { 197 | BuildpackInfo 198 | Stacks []Stack `toml:"stacks" json:"stacks,omitempty"` 199 | } 200 | -------------------------------------------------------------------------------- /buildpacks/create_buildpackage_test.go: -------------------------------------------------------------------------------- 1 | package buildpacks 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | ) 7 | 8 | func TestCreate(t *testing.T) { 9 | _, err := buildpackage("org.cloudfoundry.go-compiler", "0.0.83", "cloudfoundry/cnb:bionic", "localhost:5001/rewrite") 10 | require.NoError(t, err) 11 | } 12 | -------------------------------------------------------------------------------- /buildpacks/new_version.go: -------------------------------------------------------------------------------- 1 | package buildpacks 2 | 3 | import ( 4 | "github.com/Masterminds/semver/v3" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func newVersion(id, v string) (string, error) { 9 | version, err := semver.NewVersion(v) 10 | if err != nil { 11 | return "", errors.Errorf("could not calculate next version for %s@%s. Is it valid semver?", id, version) 12 | } 13 | 14 | return version.IncPatch().String(), nil 15 | } 16 | -------------------------------------------------------------------------------- /buildpacks/rewrite_layer.go: -------------------------------------------------------------------------------- 1 | package buildpacks 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "github.com/BurntSushi/toml" 7 | v1 "github.com/google/go-containerregistry/pkg/v1" 8 | "github.com/google/go-containerregistry/pkg/v1/tarball" 9 | "github.com/pkg/errors" 10 | "io/ioutil" 11 | "path" 12 | "strings" 13 | ) 14 | 15 | func rewriteLayer(layer v1.Layer, old, new string) (v1.Layer, error) { 16 | b := &bytes.Buffer{} 17 | tw := tar.NewWriter(b) 18 | 19 | uncompressed, err := layer.Uncompressed() 20 | if err != nil { 21 | return nil, err 22 | } 23 | defer uncompressed.Close() 24 | 25 | tr := tar.NewReader(uncompressed) 26 | for { 27 | header, err := tr.Next() 28 | if err != nil { 29 | break 30 | } 31 | 32 | newName := strings.ReplaceAll(header.Name, old, new) 33 | 34 | if strings.HasSuffix(path.Clean(header.Name), "buildpack.toml") { 35 | buf, err := ioutil.ReadAll(tr) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | bd := BuildpackDescriptor{} 41 | _, err = toml.Decode(string(buf), &bd) 42 | if err != nil { 43 | return nil, errors.Wrap(err, "decoding buildpack.toml") 44 | } 45 | 46 | bd.Info.Version = new 47 | 48 | bd.Order, err = calculateNewOrder(bd) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | updatedBuildpackToml := &bytes.Buffer{} 54 | err = toml.NewEncoder(updatedBuildpackToml).Encode(bd) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | contents := updatedBuildpackToml.Bytes() 60 | header.Name = newName 61 | header.Size = int64(len(contents)) 62 | err = tw.WriteHeader(header) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | _, err = tw.Write(contents) 68 | if err != nil { 69 | return nil, err 70 | } 71 | } else { 72 | header.Name = newName 73 | err = tw.WriteHeader(header) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | buf, err := ioutil.ReadAll(tr) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | _, err = tw.Write(buf) 84 | if err != nil { 85 | return nil, err 86 | } 87 | } 88 | 89 | } 90 | 91 | return tarball.LayerFromReader(b) 92 | } 93 | 94 | func calculateNewOrder(bd BuildpackDescriptor) (Order, error) { 95 | var newOrder Order 96 | for _, oe := range bd.Order { 97 | var newGroup []BuildpackRef 98 | for _, g := range oe.Group { 99 | newVersion, err := newVersion(g.Id, g.Version) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | newGroup = append(newGroup, BuildpackRef{ 105 | BuildpackInfo: BuildpackInfo{ 106 | Id: g.Id, 107 | Version: newVersion, 108 | }, 109 | Optional: g.Optional, 110 | }) 111 | } 112 | newOrder = append(newOrder, OrderEntry{Group: newGroup}) 113 | } 114 | return newOrder, nil 115 | } 116 | 117 | type BuildpackDescriptor struct { 118 | API string `toml:"api"` 119 | Info BuildpackTomlInfo `toml:"buildpack"` 120 | Stacks interface{} `toml:"stacks"` 121 | Order Order `toml:"order"` 122 | Metadata interface{} `toml:"metadata"` 123 | } 124 | 125 | type BuildpackTomlInfo struct { 126 | ID string `toml:"id"` 127 | Version string `toml:"version"` 128 | Name string `toml:"name"` 129 | ClearEnv bool `toml:"clear-env,omitempty"` 130 | } 131 | -------------------------------------------------------------------------------- /buildpacks/update_buildpack.go: -------------------------------------------------------------------------------- 1 | package buildpacks 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/Masterminds/semver/v3" 9 | "github.com/google/go-containerregistry/pkg/name" 10 | "github.com/pivotal/kpack/pkg/apis/build/v1alpha1" 11 | "github.com/pivotal/kpack/pkg/client/clientset/versioned" 12 | "github.com/pkg/errors" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | 15 | "github.com/matthewmcnew/kpdemo/defaults" 16 | "github.com/matthewmcnew/kpdemo/k8s" 17 | ) 18 | 19 | func UpdateBuildpack(id string) error { 20 | clusterConfig, err := k8s.BuildConfigFromFlags("", "") 21 | if err != nil { 22 | return err 23 | } 24 | 25 | client, err := versioned.NewForConfig(clusterConfig) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | builder, err := client.KpackV1alpha1().ClusterBuilders().Get(defaults.ClusterBuilderName, metav1.GetOptions{}) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | if !isInBuilder(builder, id) { 36 | return fmt.Errorf("%s is not in builder order definition use a buildpack in order:\n\n%s", id, prettyPrint(builder.Spec.Order)) 37 | } 38 | 39 | store, err := client.KpackV1alpha1().ClusterStores().Get(defaults.StoreName, metav1.GetOptions{}) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | storeBuildpack, err := findBuildpack(store.Status.Buildpacks, id) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | reference, err := name.ParseReference(builder.Spec.Tag) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | newVersion, err := newVersion(storeBuildpack.Id, storeBuildpack.Version) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | fmt.Printf("Creating new buildpack %s with version %s \n", id, newVersion) 60 | fmt.Printf("\n This will take a moment...\n") 61 | 62 | newBp, err := buildpackage(id, storeBuildpack.Version, storeBuildpack.StoreImage.Image, fmt.Sprintf("%s/%s:%s", reference.Context().RegistryStr(), reference.Context().RepositoryStr(), newVersion)) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | fmt.Printf("wrote to %s \n", newBp) 68 | 69 | store.Spec.Sources = append(store.Spec.Sources, v1alpha1.StoreImage{Image: newBp}) 70 | _, err = client.KpackV1alpha1().ClusterStores().Update(store) 71 | return err 72 | } 73 | 74 | func isInBuilder(builder *v1alpha1.ClusterBuilder, id string) bool { 75 | for _, oe := range builder.Spec.Order { 76 | for _, bp := range oe.Group { 77 | if bp.Id == id { 78 | return true 79 | } 80 | } 81 | } 82 | return false 83 | } 84 | 85 | func findBuildpack(storeBps []v1alpha1.StoreBuildpack, id string) (v1alpha1.StoreBuildpack, error) { 86 | var matchingBuildpacks []v1alpha1.StoreBuildpack 87 | for _, buildpack := range storeBps { 88 | if buildpack.Id == id { 89 | matchingBuildpacks = append(matchingBuildpacks, buildpack) 90 | } 91 | } 92 | 93 | if len(matchingBuildpacks) == 0 { 94 | return v1alpha1.StoreBuildpack{}, errors.Errorf("could not find buildpack with id '%s'", id) 95 | } 96 | 97 | return highestVersion(matchingBuildpacks) 98 | } 99 | 100 | func highestVersion(matchingBuildpacks []v1alpha1.StoreBuildpack) (v1alpha1.StoreBuildpack, error) { 101 | for _, bp := range matchingBuildpacks { 102 | if _, err := semver.NewVersion(bp.Version); err != nil { 103 | return v1alpha1.StoreBuildpack{}, errors.Errorf("cannot find buildpack '%s' with latest version due to invalid semver '%s'", bp.Id, bp.Version) 104 | } 105 | } 106 | sort.Sort(byBuildpackVersion(matchingBuildpacks)) 107 | return matchingBuildpacks[len(matchingBuildpacks)-1], nil 108 | } 109 | 110 | type byBuildpackVersion []v1alpha1.StoreBuildpack 111 | 112 | func (b byBuildpackVersion) Len() int { 113 | return len(b) 114 | } 115 | 116 | func (b byBuildpackVersion) Swap(i, j int) { 117 | b[i], b[j] = b[j], b[i] 118 | } 119 | 120 | func (b byBuildpackVersion) Less(i, j int) bool { 121 | return semver.MustParse(b[i].Version).LessThan(semver.MustParse(b[j].Version)) 122 | } 123 | 124 | func prettyPrint(order []v1alpha1.OrderEntry) string { 125 | sb := strings.Builder{} 126 | for _, oe := range order { 127 | sb.WriteString("Order:\n") 128 | for _, bp := range oe.Group { 129 | sb.WriteString(fmt.Sprintf(" %s\n", bp.Id)) 130 | } 131 | } 132 | return sb.String() 133 | } 134 | -------------------------------------------------------------------------------- /cmd/kpdemo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/matthewmcnew/kpdemo/buildpacks" 11 | "github.com/matthewmcnew/kpdemo/logs" 12 | "github.com/matthewmcnew/kpdemo/populate" 13 | "github.com/matthewmcnew/kpdemo/rebase" 14 | "github.com/matthewmcnew/kpdemo/server" 15 | ) 16 | 17 | var rootCmd = &cobra.Command{ 18 | Use: "", 19 | Short: "A tool to demo kpack", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | fmt.Println("Welcome to the kpack Demo") 22 | }, 23 | } 24 | 25 | func main() { 26 | _ = rootCmd.Execute() 27 | } 28 | 29 | func init() { 30 | rootCmd.AddCommand(populateCmd(), 31 | serveCmd(), 32 | updateRunImageCmd(), 33 | cleanupCmd(), 34 | logsCmd(), 35 | updateBPCmd(), 36 | ) 37 | } 38 | 39 | func populateCmd() *cobra.Command { 40 | var registry string 41 | var cacheSize string 42 | var count int32 43 | var cmd = &cobra.Command{ 44 | Use: "populate", 45 | Aliases: []string{"setup"}, 46 | Short: "Populate kpack with images", 47 | RunE: func(cmd *cobra.Command, args []string) error { 48 | fmt.Println("Relocating Buildpacks and Run Image. This will take a moment.") 49 | imageTag := fmt.Sprintf("%s/kpdemo", registry) 50 | 51 | fmt.Printf("Writing all images to: %s\n", imageTag) 52 | 53 | relocated, err := populate.Relocate(imageTag) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | return populate.Populate(count, relocated.Order, imageTag, cacheSize) 59 | }, 60 | } 61 | cmd.Flags().StringVarP(&cacheSize, "cache-size", "s", "500Mi", "the cache size to use for kpack images") 62 | 63 | cmd.Flags().StringVarP(®istry, "registry", "r", "", "registry to deploy images into") 64 | _ = cmd.MarkFlagRequired("registry") 65 | 66 | cmd.Flags().Int32VarP(&count, "count", "c", 0, "the number of images to populate in kpack") 67 | _ = cmd.MarkFlagRequired("count") 68 | 69 | return cmd 70 | } 71 | 72 | func updateBPCmd() *cobra.Command { 73 | var buildpack string 74 | var cmd = &cobra.Command{ 75 | Use: "update-buildpack", 76 | Short: "Create new buildpack to simulate update", 77 | RunE: func(cmd *cobra.Command, args []string) error { 78 | return buildpacks.UpdateBuildpack(buildpack) 79 | }, 80 | } 81 | cmd.Flags().StringVarP(&buildpack, "buildpack", "b", "", "the id of the buildpack to update") 82 | _ = cmd.MarkFlagRequired("buildpack") 83 | 84 | return cmd 85 | } 86 | 87 | func serveCmd() *cobra.Command { 88 | var port string 89 | var cmd = &cobra.Command{ 90 | Use: "serve", 91 | Aliases: []string{"visualization", "ui"}, 92 | Short: "Setup a local web server for the kpack visualization", 93 | RunE: func(cmd *cobra.Command, args []string) error { 94 | fmt.Println("Starting Up") 95 | go func() { 96 | time.Sleep(500 * time.Millisecond) 97 | 98 | url := fmt.Sprintf("http://localhost:%s", port) 99 | fmt.Printf("Open up a browser to %s\n", url) 100 | 101 | server.OpenBrowser(url) 102 | }() 103 | 104 | server.Serve(port) 105 | 106 | return nil 107 | }, 108 | } 109 | 110 | cmd.Flags().StringVarP(&port, "port", "p", "8080", "registry to deploy images into") 111 | 112 | return cmd 113 | } 114 | 115 | func updateRunImageCmd() *cobra.Command { 116 | var cmd = &cobra.Command{ 117 | Use: "update-stack", 118 | Aliases: []string{"rebase", "update-run-image", "stack-update"}, 119 | Short: "Demo an update by pushing an updated stack run image", 120 | RunE: func(cmd *cobra.Command, args []string) error { 121 | return rebase.UpdateRunImage() 122 | }, 123 | } 124 | 125 | return cmd 126 | } 127 | 128 | func cleanupCmd() *cobra.Command { 129 | var cmd = &cobra.Command{ 130 | Use: "cleanup", 131 | Short: "Remove kpack demo images", 132 | RunE: func(cmd *cobra.Command, args []string) error { 133 | return populate.Cleanup() 134 | }, 135 | } 136 | 137 | return cmd 138 | } 139 | 140 | func logsCmd() *cobra.Command { 141 | var cmd = &cobra.Command{ 142 | Use: "logs", 143 | Short: "Stream build logs from an image", 144 | Example: "kpdemo logs ", 145 | RunE: func(cmd *cobra.Command, args []string) error { 146 | if len(args) < 1 { 147 | return errors.New("no image name provided") 148 | } 149 | 150 | image := args[0] 151 | 152 | return logs.Logs(image) 153 | }, 154 | } 155 | 156 | return cmd 157 | } 158 | -------------------------------------------------------------------------------- /defaults/demo.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | const ( 4 | ClusterBuilderName = "default" 5 | StackName = "default" 6 | StoreName = "default" 7 | Namespace = "demo-project" 8 | OldSpecAnnotation = "kpdemo/old-spec" 9 | ) 10 | -------------------------------------------------------------------------------- /docs/assets/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthewmcnew/kpdemo/9fbcb3aeeb7e8f445532c1840db1f2ec975946f5/docs/assets/sample.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/matthewmcnew/kpdemo 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/Masterminds/semver/v3 v3.1.0 8 | github.com/apex/log v1.3.0 9 | github.com/fatih/color v1.9.0 10 | github.com/google/go-containerregistry v0.1.1 11 | github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e 12 | github.com/pivotal/kpack v0.1.0 13 | github.com/pkg/errors v0.9.1 14 | github.com/rakyll/statik v0.1.6 15 | github.com/spf13/cobra v1.0.0 16 | github.com/stretchr/testify v1.6.1 17 | k8s.io/api v0.17.6 18 | k8s.io/apimachinery v0.17.6 19 | k8s.io/client-go v11.0.1-0.20190805182717-6502b5e7b1b5+incompatible 20 | ) 21 | 22 | replace ( 23 | k8s.io/client-go => k8s.io/client-go v0.17.6 24 | ) 25 | -------------------------------------------------------------------------------- /images/current.go: -------------------------------------------------------------------------------- 1 | package images 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/apex/log" 8 | "github.com/pivotal/kpack/pkg/apis/build/v1alpha1" 9 | corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" 10 | v1alpha1Listers "github.com/pivotal/kpack/pkg/client/listers/build/v1alpha1" 11 | "github.com/pkg/errors" 12 | "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/labels" 14 | _ "k8s.io/client-go/plugin/pkg/client/auth" 15 | ) 16 | 17 | type Image struct { 18 | Name string `json:"name"` 19 | Namespace string `json:"namespace"` 20 | BuildCount int64 `json:"buildCount"` 21 | Status string `json:"status"` 22 | BuildMetadata v1alpha1.BuildpackMetadataList `json:"buildMetadata"` 23 | Completed int `json:"completed"` 24 | Remaining int `json:"remaining"` 25 | CreatedAt time.Time `json:"createdAt"` 26 | Tag string `json:"tag"` 27 | LatestImage string `json:"latestImage"` 28 | RunImage string `json:"runImage"` 29 | LastBuildStatus string `json:"lastBuildStatus"` 30 | } 31 | 32 | func Current(lister v1alpha1Listers.ImageLister, buildLister v1alpha1Listers.BuildLister) ([]Image, error) { 33 | kpackImages, err := lister.List(labels.Everything()) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | images := make([]Image, 0, len(kpackImages)) 39 | for _, i := range kpackImages { 40 | 41 | lastCompletedBuild, err := lastCompletedBuild(buildLister, i) 42 | if err != nil { 43 | log.Info(err.Error()) 44 | continue 45 | } 46 | 47 | done, remaining := remaining(buildLister, i) 48 | images = append(images, Image{ 49 | Name: i.Name, 50 | Tag: i.Spec.Tag, 51 | LatestImage: i.Status.LatestImage, 52 | Namespace: i.Namespace, 53 | BuildCount: i.Status.BuildCounter, 54 | Status: status(i), 55 | LastBuildStatus: buildStatus(lastCompletedBuild), 56 | CreatedAt: i.CreationTimestamp.Time, 57 | Completed: done, 58 | Remaining: remaining, 59 | BuildMetadata: lastCompletedBuild.Status.BuildMetadata, 60 | RunImage: lastCompletedBuild.Status.Stack.RunImage, 61 | }) 62 | } 63 | 64 | return images, nil 65 | } 66 | 67 | func lastCompletedBuild(buildLister v1alpha1Listers.BuildLister, image *v1alpha1.Image) (*v1alpha1.Build, error) { 68 | buildRef := image.Status.LatestBuildRef 69 | if buildRef == "" { 70 | return nil, errors.Errorf("build not ready yet :): %s", image.Name) 71 | } 72 | 73 | key := image.Name + "-" + image.Namespace 74 | if image.Status.LatestImage != "" && image.Status.GetCondition(corev1alpha1.ConditionReady).IsUnknown() { 75 | var ok bool 76 | buildRef, ok = cache[key] 77 | if !ok { 78 | return nil, errors.New("coulding find cache key for build") 79 | } 80 | } 81 | 82 | build, err := buildLister.Builds(image.Namespace).Get(buildRef) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | cacheMutex.Lock() 88 | defer cacheMutex.Unlock() 89 | cache[key] = buildRef 90 | 91 | return build, nil 92 | } 93 | 94 | var cacheMutex = &sync.Mutex{} 95 | var cache = map[string]string{} 96 | 97 | func remaining(buildLister v1alpha1Listers.BuildLister, image *v1alpha1.Image) (int, int) { 98 | if image.Status.LatestBuildRef == "" { 99 | return 0, 10 100 | } 101 | 102 | if image.Status.GetCondition(corev1alpha1.ConditionReady).IsTrue() { 103 | return 1, 1 104 | } 105 | 106 | if image.Status.GetCondition(corev1alpha1.ConditionReady).IsFalse() { 107 | return 1, 1 108 | } 109 | 110 | //todo short circuit 111 | 112 | build, err := buildLister.Builds(image.Namespace).Get(image.Status.LatestBuildRef) 113 | if err != nil { 114 | return 0, -1 115 | } 116 | 117 | if len(build.Status.StepStates) == 0 { 118 | return 0, -1 119 | } 120 | 121 | return len(build.Status.StepsCompleted), len(build.Status.StepStates) 122 | } 123 | 124 | func status(image *v1alpha1.Image) string { 125 | condition := image.Status.GetCondition(corev1alpha1.ConditionReady) 126 | if condition == nil { 127 | return string(v1.ConditionUnknown) 128 | } 129 | 130 | return string(condition.Status) 131 | } 132 | 133 | func buildStatus(build *v1alpha1.Build) string { 134 | condition := build.Status.GetCondition(corev1alpha1.ConditionSucceeded) 135 | if condition == nil { 136 | return string(v1.ConditionUnknown) 137 | } 138 | 139 | return string(condition.Status) 140 | } 141 | -------------------------------------------------------------------------------- /k8s/config.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "k8s.io/client-go/rest" 5 | "k8s.io/client-go/tools/clientcmd" 6 | "k8s.io/client-go/tools/clientcmd/api" 7 | ) 8 | 9 | func BuildConfigFromFlags(masterURL, kubeconfigPath string) (*rest.Config, error) { 10 | 11 | var clientConfigLoader clientcmd.ClientConfigLoader 12 | 13 | if kubeconfigPath == "" { 14 | clientConfigLoader = clientcmd.NewDefaultClientConfigLoadingRules() 15 | } else { 16 | clientConfigLoader = &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath} 17 | } 18 | 19 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 20 | clientConfigLoader, 21 | &clientcmd.ConfigOverrides{ClusterInfo: api.Cluster{Server: masterURL}}).ClientConfig() 22 | 23 | } 24 | -------------------------------------------------------------------------------- /logs/logs.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/pivotal/kpack/pkg/apis/build/v1alpha1" 8 | "github.com/pivotal/kpack/pkg/client/clientset/versioned" 9 | "github.com/pivotal/kpack/pkg/logs" 10 | "github.com/pkg/errors" 11 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/client-go/kubernetes" 13 | 14 | "github.com/matthewmcnew/kpdemo/k8s" 15 | ) 16 | 17 | func Logs(name string) error { 18 | clusterConfig, err := k8s.BuildConfigFromFlags("", "") 19 | if err != nil { 20 | return err 21 | } 22 | 23 | k8sClient, err := kubernetes.NewForConfig(clusterConfig) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | client, err := versioned.NewForConfig(clusterConfig) 29 | if err != nil { 30 | return errors.Wrapf(err, "building kubeconfig") 31 | } 32 | 33 | list, err := client.KpackV1alpha1().Images("").List(v1.ListOptions{}) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | image, ok := find(list, name) 39 | if !ok { 40 | return errors.Errorf("could not find image: %s", name) 41 | } 42 | 43 | return logs.NewBuildLogsClient(k8sClient).Tail(context.Background(), os.Stdout, image.Name, "", image.Namespace) 44 | } 45 | 46 | func find(list *v1alpha1.ImageList, name string) (*v1alpha1.Image, bool) { 47 | for _, i := range list.Items { 48 | if i.Name == name { 49 | return &i, true 50 | } 51 | } 52 | return nil, false 53 | } 54 | -------------------------------------------------------------------------------- /populate/cleanup.go: -------------------------------------------------------------------------------- 1 | package populate 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/pivotal/kpack/pkg/apis/build/v1alpha1" 8 | "github.com/pivotal/kpack/pkg/client/clientset/versioned" 9 | "github.com/pkg/errors" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | 13 | "github.com/matthewmcnew/kpdemo/defaults" 14 | "github.com/matthewmcnew/kpdemo/k8s" 15 | ) 16 | 17 | func Cleanup() error { 18 | clusterConfig, err := k8s.BuildConfigFromFlags("", "") 19 | if err != nil { 20 | return err 21 | } 22 | 23 | k8sclient, err := kubernetes.NewForConfig(clusterConfig) 24 | if err != nil { 25 | return errors.Wrapf(err, "building kubeconfig") 26 | } 27 | 28 | client, err := versioned.NewForConfig(clusterConfig) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | images, err := client.KpackV1alpha1().Images(defaults.Namespace).List(metav1.ListOptions{}) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | fmt.Printf("Removing %d images\n", len(images.Items)) 39 | 40 | for _, i := range images.Items { 41 | deleteBackground := metav1.DeletePropagationBackground 42 | err := client.KpackV1alpha1().Images(defaults.Namespace).Delete(i.Name, &metav1.DeleteOptions{ 43 | PropagationPolicy: &deleteBackground, 44 | }) 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | 50 | err = k8sclient.CoreV1().Namespaces().Delete(defaults.Namespace, &metav1.DeleteOptions{}) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | err = deleteStack(client) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | err = deleteStore(client) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return deleteBuilder(client) 66 | } 67 | 68 | func deleteStack(client *versioned.Clientset) error { 69 | stack, err := client.KpackV1alpha1().ClusterStacks().Get(defaults.StackName, metav1.GetOptions{}) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | oldSpec, ok := stack.Annotations[defaults.OldSpecAnnotation] 75 | if ok { 76 | stackSpec := v1alpha1.ClusterStackSpec{} 77 | err := json.Unmarshal([]byte(oldSpec), &stackSpec) 78 | if err != nil { 79 | return err 80 | } 81 | delete(stack.Annotations, defaults.OldSpecAnnotation) 82 | stack.Spec = stackSpec 83 | 84 | _, err = client.KpackV1alpha1().ClusterStacks().Update(stack) 85 | } else { 86 | err = client.KpackV1alpha1().ClusterStacks().Delete(defaults.StackName, &metav1.DeleteOptions{}) 87 | } 88 | return err 89 | } 90 | 91 | func deleteStore(client *versioned.Clientset) error { 92 | store, err := client.KpackV1alpha1().ClusterStores().Get(defaults.StoreName, metav1.GetOptions{}) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | oldSpec, ok := store.Annotations[defaults.OldSpecAnnotation] 98 | if ok { 99 | storeSpec := v1alpha1.ClusterStoreSpec{} 100 | err := json.Unmarshal([]byte(oldSpec), &storeSpec) 101 | if err != nil { 102 | return err 103 | } 104 | delete(store.Annotations, defaults.OldSpecAnnotation) 105 | store.Spec = storeSpec 106 | 107 | _, err = client.KpackV1alpha1().ClusterStores().Update(store) 108 | } else { 109 | err = client.KpackV1alpha1().ClusterStores().Delete(defaults.StoreName, &metav1.DeleteOptions{}) 110 | } 111 | return err 112 | } 113 | 114 | func deleteBuilder(client *versioned.Clientset) error { 115 | builder, err := client.KpackV1alpha1().ClusterBuilders().Get(defaults.ClusterBuilderName, metav1.GetOptions{}) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | oldSpec, ok := builder.Annotations[defaults.OldSpecAnnotation] 121 | if ok { 122 | builderSpec := v1alpha1.ClusterBuilderSpec{} 123 | err := json.Unmarshal([]byte(oldSpec), &builderSpec) 124 | if err != nil { 125 | return err 126 | } 127 | delete(builder.Annotations, defaults.OldSpecAnnotation) 128 | builder.Spec = builderSpec 129 | 130 | _, err = client.KpackV1alpha1().ClusterBuilders().Update(builder) 131 | } else { 132 | err = client.KpackV1alpha1().ClusterBuilders().Delete(defaults.ClusterBuilderName, &metav1.DeleteOptions{}) 133 | } 134 | return err 135 | } 136 | -------------------------------------------------------------------------------- /populate/populate.go: -------------------------------------------------------------------------------- 1 | package populate 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "time" 9 | 10 | "github.com/google/go-containerregistry/pkg/authn" 11 | "github.com/google/go-containerregistry/pkg/name" 12 | "github.com/goombaio/namegenerator" 13 | "github.com/pivotal/kpack/pkg/apis/build/v1alpha1" 14 | "github.com/pivotal/kpack/pkg/client/clientset/versioned" 15 | "github.com/pkg/errors" 16 | v1 "k8s.io/api/core/v1" 17 | k8errors "k8s.io/apimachinery/pkg/api/errors" 18 | "k8s.io/apimachinery/pkg/api/resource" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | "k8s.io/client-go/kubernetes" 21 | 22 | "github.com/matthewmcnew/kpdemo/defaults" 23 | "github.com/matthewmcnew/kpdemo/k8s" 24 | ) 25 | 26 | func Populate(count int32, order v1alpha1.Order, imageTag, cacheSize string) error { 27 | clusterConfig, err := k8s.BuildConfigFromFlags("", "") 28 | if err != nil { 29 | return errors.Wrapf(err, "building kubeconfig") 30 | } 31 | 32 | k8sclient, err := kubernetes.NewForConfig(clusterConfig) 33 | if err != nil { 34 | return errors.Wrapf(err, "building kubeconfig") 35 | } 36 | 37 | client, err := versioned.NewForConfig(clusterConfig) 38 | if err != nil { 39 | return errors.Wrapf(err, "building kubeconfig") 40 | } 41 | 42 | c, err := loadConfig(count, imageTag) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | _, err = k8sclient.CoreV1().Namespaces().Create(&v1.Namespace{ 48 | ObjectMeta: metav1.ObjectMeta{ 49 | Name: defaults.Namespace, 50 | }, 51 | }) 52 | if err != nil && !k8errors.IsAlreadyExists(err) { 53 | return err 54 | } 55 | 56 | secret, err := k8sclient.CoreV1().Secrets(defaults.Namespace).Create(&v1.Secret{ 57 | ObjectMeta: metav1.ObjectMeta{ 58 | GenerateName: "kpdemo-dockersecret-", 59 | Annotations: map[string]string{ 60 | "kpack.io/docker": c.registry, 61 | }, 62 | }, 63 | StringData: map[string]string{ 64 | "username": c.username, 65 | "password": c.password, 66 | }, 67 | Type: v1.SecretTypeBasicAuth, 68 | }) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | serviceAccount, err := k8sclient.CoreV1().ServiceAccounts(defaults.Namespace).Create(&v1.ServiceAccount{ 74 | ObjectMeta: metav1.ObjectMeta{ 75 | GenerateName: "kpdemo-serviceaccount-", 76 | }, 77 | Secrets: []v1.ObjectReference{ 78 | { 79 | Name: secret.Name, 80 | }, 81 | }, 82 | }) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | err = saveBuilder(client, &v1alpha1.ClusterBuilder{ 88 | ObjectMeta: metav1.ObjectMeta{ 89 | Name: defaults.ClusterBuilderName, 90 | }, 91 | Spec: v1alpha1.ClusterBuilderSpec{ 92 | BuilderSpec: v1alpha1.BuilderSpec{ 93 | Tag: fmt.Sprintf("%s:%s", c.imageTag, "builder"), 94 | Stack: v1.ObjectReference{ 95 | Name: defaults.StackName, 96 | Kind: "ClusterStack", 97 | }, 98 | Store: v1.ObjectReference{ 99 | Name: defaults.StoreName, 100 | Kind: "ClusterStore", 101 | }, 102 | Order: order, 103 | }, 104 | ServiceAccountRef: v1.ObjectReference{ 105 | Namespace: serviceAccount.Namespace, 106 | Name: serviceAccount.Name, 107 | }, 108 | }, 109 | }) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | cache, err := resource.ParseQuantity(cacheSize) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | nameGenerator := namegenerator.NewNameGenerator(time.Now().UTC().UnixNano()) 120 | for i := 1; i <= c.count; i++ { 121 | sourceConfig, tag := randomSourceConfig() 122 | image, err := client.KpackV1alpha1().Images(defaults.Namespace).Create(&v1alpha1.Image{ 123 | ObjectMeta: metav1.ObjectMeta{ 124 | Name: nameGenerator.Generate(), 125 | }, 126 | Spec: v1alpha1.ImageSpec{ 127 | Tag: fmt.Sprintf("%s:%s", c.imageTag, tag), 128 | Builder: v1.ObjectReference{ 129 | Name: defaults.ClusterBuilderName, 130 | Kind: "ClusterBuilder", 131 | }, 132 | ServiceAccount: serviceAccount.Name, 133 | Source: sourceConfig, 134 | CacheSize: &cache, 135 | ImageTaggingStrategy: v1alpha1.None, 136 | }, 137 | }) 138 | if err != nil && !k8errors.IsAlreadyExists(err) { 139 | return err 140 | } else if k8errors.IsAlreadyExists(err) { 141 | i-- 142 | continue 143 | } 144 | 145 | log.Printf("created image %s", image.Name) 146 | time.Sleep(3 * time.Second) 147 | } 148 | return nil 149 | } 150 | 151 | type config struct { 152 | builder string 153 | imageTag string 154 | username string 155 | password string 156 | registry string 157 | count int 158 | } 159 | 160 | func loadConfig(count int32, imageTag string) (config, error) { 161 | reg, err := name.ParseReference(imageTag, name.WeakValidation) 162 | if err != nil { 163 | return config{}, errors.Wrapf(err, "could not parse %s", imageTag) 164 | } 165 | 166 | auth, err := authn.DefaultKeychain.Resolve(reg.Context().Registry) 167 | if err != nil { 168 | return config{}, errors.Wrapf(err, "could not find registry", imageTag) 169 | } 170 | 171 | basicAuth, err := auth.Authorization() 172 | if err != nil { 173 | return config{}, errors.Wrapf(err, "could not get auth for imge", imageTag) 174 | } 175 | 176 | return config{ 177 | username: basicAuth.Username, 178 | password: basicAuth.Password, 179 | count: int(count), 180 | imageTag: imageTag, 181 | registry: func() string { 182 | if reg.Context().RegistryStr() == name.DefaultRegistry { 183 | return "https://" + name.DefaultRegistry + "/v1/" 184 | } 185 | return reg.Context().RegistryStr() 186 | }(), 187 | }, nil 188 | } 189 | 190 | func randomSourceConfig() (v1alpha1.SourceConfig, string) { 191 | rand.Seed(time.Now().UnixNano()) 192 | sourceConfigs := []v1alpha1.SourceConfig{ 193 | { 194 | Git: &v1alpha1.Git{ 195 | URL: "https://github.com/matthewmcnew/sample-java-app", 196 | Revision: "dbba68cee6473b5df51a1a43806d920d2ed4e4ee", 197 | }, 198 | }, 199 | { 200 | Git: &v1alpha1.Git{ 201 | URL: "https://github.com/matthewmcnew/build-samples", 202 | Revision: "a94df327e098fe924b06547a1adf9c3cda5684c9", 203 | }, 204 | }, 205 | { 206 | Git: &v1alpha1.Git{ 207 | URL: "https://github.com/paketo-buildpacks/nginx", 208 | Revision: "85f4a1e8ec3ae774ade1bfae3a886b6ae7865303", 209 | }, 210 | SubPath: "integration/testdata/simple_app", 211 | }, 212 | { 213 | Git: &v1alpha1.Git{ 214 | URL: "https://github.com/paketo-buildpacks/dotnet-core-runtime", 215 | Revision: "9ff9b56e88bf674391b2609b4dadeea28599da6a", 216 | }, 217 | SubPath: "integration/testdata/simple_app", 218 | }, 219 | } 220 | 221 | imageTypes := []string{ 222 | "java", 223 | "node", 224 | "go", 225 | "dotnet", 226 | } 227 | 228 | randomIndex := rand.Intn(len(sourceConfigs)) 229 | 230 | return sourceConfigs[randomIndex], imageTypes[randomIndex] 231 | } 232 | 233 | func saveBuilder(client *versioned.Clientset, builder *v1alpha1.ClusterBuilder) error { 234 | 235 | var order []v1alpha1.OrderEntry 236 | for _, o := range builder.Spec.Order { 237 | var group []v1alpha1.BuildpackRef 238 | for _, g := range o.Group { 239 | group = append(group, v1alpha1.BuildpackRef{ 240 | BuildpackInfo: v1alpha1.BuildpackInfo{ 241 | Id: g.Id, 242 | }, 243 | Optional: g.Optional, 244 | }) 245 | } 246 | 247 | order = append(order, v1alpha1.OrderEntry{ 248 | Group: group, 249 | }) 250 | } 251 | builder.Spec.Order = order 252 | 253 | existingBuilder, err := client.KpackV1alpha1().ClusterBuilders().Get(defaults.ClusterBuilderName, metav1.GetOptions{}) 254 | if err != nil && !k8errors.IsNotFound(err) { 255 | return err 256 | } 257 | if k8errors.IsNotFound(err) { 258 | _, err = client.KpackV1alpha1().ClusterBuilders().Create(builder) 259 | } else { 260 | oldSpec, err := json.Marshal(existingBuilder.Spec) 261 | if err != nil { 262 | return err 263 | } 264 | 265 | if existingBuilder.Annotations == nil { 266 | existingBuilder.Annotations = map[string]string{} 267 | } 268 | 269 | existingBuilder.Annotations[defaults.OldSpecAnnotation] = string(oldSpec) 270 | existingBuilder.Spec = builder.Spec 271 | _, err = client.KpackV1alpha1().ClusterBuilders().Update(existingBuilder) 272 | } 273 | return err 274 | } 275 | -------------------------------------------------------------------------------- /populate/relocate.go: -------------------------------------------------------------------------------- 1 | package populate 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/fatih/color" 7 | "github.com/google/go-containerregistry/pkg/authn" 8 | "github.com/google/go-containerregistry/pkg/name" 9 | v1 "github.com/google/go-containerregistry/pkg/v1" 10 | "github.com/google/go-containerregistry/pkg/v1/remote" 11 | "github.com/pivotal/kpack/pkg/apis/build/v1alpha1" 12 | "github.com/pivotal/kpack/pkg/client/clientset/versioned" 13 | "github.com/pivotal/kpack/pkg/registry/imagehelpers" 14 | "github.com/pkg/errors" 15 | k8errors "k8s.io/apimachinery/pkg/api/errors" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/client-go/kubernetes" 18 | 19 | "github.com/matthewmcnew/kpdemo/defaults" 20 | "github.com/matthewmcnew/kpdemo/k8s" 21 | ) 22 | 23 | const ( 24 | kpackNamespace = "kpack" 25 | kpConfigMap = "kp-config" 26 | canonRepoKey = "canonical.repository" 27 | ) 28 | 29 | type Relocated struct { 30 | Order v1alpha1.Order 31 | } 32 | 33 | func Relocate(imageTag string) (Relocated, error) { 34 | clusterConfig, err := k8s.BuildConfigFromFlags("", "") 35 | if err != nil { 36 | return Relocated{}, errors.Wrapf(err, "building kubeconfig") 37 | } 38 | 39 | k8sClient, err := kubernetes.NewForConfig(clusterConfig) 40 | if err != nil { 41 | return Relocated{}, errors.Wrapf(err, "building kubeconfig") 42 | } 43 | 44 | client, err := versioned.NewForConfig(clusterConfig) 45 | if err != nil { 46 | return Relocated{}, errors.Wrapf(err, "building kubeconfig") 47 | } 48 | 49 | runRef, err := name.ParseReference("paketobuildpacks/run:base-cnb") 50 | if err != nil { 51 | return Relocated{}, err 52 | } 53 | 54 | run, err := remote.Image(runRef, remote.WithAuthFromKeychain(authn.DefaultKeychain)) 55 | if err != nil { 56 | return Relocated{}, err 57 | } 58 | 59 | relocatedRunRef, err := name.ParseReference(imageTag + ":run") 60 | if err != nil { 61 | return Relocated{}, err 62 | } 63 | 64 | runImage, err := save(relocatedRunRef, run) 65 | if err != nil { 66 | return Relocated{}, err 67 | } 68 | 69 | if !verifyRegistryPublic(k8sClient, runImage) { 70 | fmt.Printf("\n%s: Image: %s is not public. \n kpdemo populate will not work if %s is not public or readable by kpack and the nodes on the cluster\n Continuing anyway...\n\n", 71 | color.RedString("WARNING"), 72 | imageTag, 73 | imageTag) 74 | } 75 | 76 | builderRef, err := name.ParseReference("paketobuildpacks/builder:base") 77 | if err != nil { 78 | return Relocated{}, err 79 | } 80 | 81 | builder, err := remote.Image(builderRef, remote.WithAuthFromKeychain(authn.DefaultKeychain)) 82 | if err != nil { 83 | return Relocated{}, err 84 | } 85 | 86 | var order []v1alpha1.OrderEntry 87 | err = imagehelpers.GetLabel(builder, "io.buildpacks.buildpack.order", &order) 88 | if err != nil { 89 | return Relocated{}, err 90 | } 91 | 92 | relocatedBuilderRef, err := name.ParseReference(imageTag + ":buildpacks") 93 | if err != nil { 94 | return Relocated{}, err 95 | } 96 | buildpacksImage, err := save(relocatedBuilderRef, builder) 97 | 98 | err = saveClusterStack(client, &v1alpha1.ClusterStack{ 99 | ObjectMeta: metav1.ObjectMeta{ 100 | Name: defaults.StackName, 101 | }, 102 | Spec: v1alpha1.ClusterStackSpec{ 103 | Id: "io.buildpacks.stacks.bionic", 104 | BuildImage: v1alpha1.ClusterStackSpecImage{ 105 | Image: "paketobuildpacks/build:base-cnb", 106 | }, 107 | RunImage: v1alpha1.ClusterStackSpecImage{ 108 | Image: runImage, 109 | }, 110 | }, 111 | }) 112 | if err != nil { 113 | return Relocated{}, err 114 | } 115 | 116 | err = saveClusterStore(client, &v1alpha1.ClusterStore{ 117 | ObjectMeta: metav1.ObjectMeta{ 118 | Name: defaults.StoreName, 119 | }, 120 | Spec: v1alpha1.ClusterStoreSpec{ 121 | Sources: []v1alpha1.StoreImage{ 122 | { 123 | Image: buildpacksImage, 124 | }, 125 | }, 126 | }, 127 | }) 128 | 129 | return Relocated{ 130 | Order: order, 131 | }, err 132 | } 133 | 134 | func save(ref name.Reference, i v1.Image) (string, error) { 135 | err := remote.Write(ref, i, remote.WithAuthFromKeychain(authn.DefaultKeychain)) 136 | if err != nil { 137 | return "", err 138 | } 139 | 140 | digest, err := i.Digest() 141 | if err != nil { 142 | return "", err 143 | } 144 | 145 | return fmt.Sprintf("%s@%s", ref.Name(), digest.String()), nil 146 | } 147 | 148 | func saveClusterStore(client *versioned.Clientset, store *v1alpha1.ClusterStore) error { 149 | existingClusterStore, err := client.KpackV1alpha1().ClusterStores().Get(defaults.StoreName, metav1.GetOptions{}) 150 | if err != nil && !k8errors.IsNotFound(err) { 151 | return err 152 | } 153 | if k8errors.IsNotFound(err) { 154 | _, err = client.KpackV1alpha1().ClusterStores().Create(store) 155 | } else { 156 | oldSpec, err := json.Marshal(existingClusterStore.Spec) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | if existingClusterStore.Annotations == nil { 162 | existingClusterStore.Annotations = map[string]string{} 163 | } 164 | 165 | existingClusterStore.Annotations[defaults.OldSpecAnnotation] = string(oldSpec) 166 | existingClusterStore.Spec = store.Spec 167 | _, err = client.KpackV1alpha1().ClusterStores().Update(existingClusterStore) 168 | } 169 | return err 170 | } 171 | 172 | func saveClusterStack(client *versioned.Clientset, stack *v1alpha1.ClusterStack) error { 173 | existingClusterStack, err := client.KpackV1alpha1().ClusterStacks().Get(defaults.StackName, metav1.GetOptions{}) 174 | if err != nil && !k8errors.IsNotFound(err) { 175 | return err 176 | } 177 | if k8errors.IsNotFound(err) { 178 | _, err = client.KpackV1alpha1().ClusterStacks().Create(stack) 179 | } else { 180 | oldSpec, err := json.Marshal(existingClusterStack.Spec) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | if existingClusterStack.Annotations == nil { 186 | existingClusterStack.Annotations = map[string]string{} 187 | } 188 | 189 | existingClusterStack.Annotations[defaults.OldSpecAnnotation] = string(oldSpec) 190 | existingClusterStack.Spec = stack.Spec 191 | _, err = client.KpackV1alpha1().ClusterStacks().Update(existingClusterStack) 192 | } 193 | return err 194 | } 195 | 196 | func verifyRegistryPublic(client *kubernetes.Clientset, image string) bool { 197 | if isBuildServiceRegistry(client, image) { 198 | return true 199 | } 200 | 201 | ref, _ := name.ParseReference(image) 202 | 203 | _, err := remote.Image(ref, remote.WithAuth(authn.Anonymous)) 204 | if err != nil { 205 | return false 206 | } 207 | 208 | return true 209 | } 210 | 211 | func isBuildServiceRegistry(client *kubernetes.Clientset, image string) bool { 212 | kpConfig, err := client.CoreV1().ConfigMaps(kpackNamespace).Get(kpConfigMap, metav1.GetOptions{}) 213 | if err != nil { 214 | return false 215 | } 216 | 217 | canonRepo, ok := kpConfig.Data[canonRepoKey] 218 | if !ok { 219 | return false 220 | } 221 | 222 | canonReg, err := name.ParseReference(canonRepo) 223 | if err != nil { 224 | return false 225 | } 226 | 227 | reg, err := name.ParseReference(image) 228 | if err != nil { 229 | return false 230 | } 231 | 232 | return canonReg.Context().RegistryStr() == reg.Context().RegistryStr() 233 | } 234 | -------------------------------------------------------------------------------- /rebase/update_run_image.go: -------------------------------------------------------------------------------- 1 | package rebase 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/google/go-containerregistry/pkg/authn" 8 | "github.com/google/go-containerregistry/pkg/name" 9 | v1 "github.com/google/go-containerregistry/pkg/v1" 10 | "github.com/google/go-containerregistry/pkg/v1/remote" 11 | "github.com/pivotal/kpack/pkg/client/clientset/versioned" 12 | "github.com/pivotal/kpack/pkg/registry/imagehelpers" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | 15 | "github.com/matthewmcnew/kpdemo/defaults" 16 | "github.com/matthewmcnew/kpdemo/k8s" 17 | ) 18 | 19 | func UpdateRunImage() error { 20 | clusterConfig, err := k8s.BuildConfigFromFlags("", "") 21 | if err != nil { 22 | return err 23 | } 24 | 25 | client, err := versioned.NewForConfig(clusterConfig) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | stack, err := client.KpackV1alpha1().ClusterStacks().Get(defaults.StackName, metav1.GetOptions{}) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | reference, err := name.ParseReference(stack.Spec.RunImage.Image) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | updateRef, err := name.ParseReference(fmt.Sprintf("%s/%s:run", reference.Context().RegistryStr(), reference.Context().RepositoryStr())) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | fmt.Printf("Pushing update to: %s\n", updateRef.Name()) 46 | 47 | i, err := remote.Image(reference, remote.WithAuthFromKeychain(authn.DefaultKeychain)) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | i, err = imagehelpers.SetStringLabel(i, "KPDEMO_DEMO", time.Now().String()) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | updatedImage, err := save(updateRef, i) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | fmt.Printf("Updated Run Image %s\n", updatedImage) 63 | 64 | stack.Spec.RunImage.Image = updatedImage 65 | _, err = client.KpackV1alpha1().ClusterStacks().Update(stack) 66 | return err 67 | } 68 | 69 | func save(ref name.Reference, i v1.Image) (string, error) { 70 | err := remote.Write(ref, i, remote.WithAuthFromKeychain(authn.DefaultKeychain)) 71 | if err != nil { 72 | return "", err 73 | } 74 | 75 | digest, err := i.Digest() 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | return fmt.Sprintf("%s@%s", ref.Name(), digest.String()), nil 81 | } 82 | -------------------------------------------------------------------------------- /server/open_browser.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "log" 5 | "os/exec" 6 | "runtime" 7 | ) 8 | 9 | func OpenBrowser(url string) { 10 | var err error 11 | 12 | switch runtime.GOOS { 13 | case "linux": 14 | err = exec.Command("xdg-open", url).Start() 15 | case "windows": 16 | err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() 17 | case "darwin": 18 | err = exec.Command("open", url).Start() 19 | default: 20 | return 21 | } 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/pivotal/kpack/pkg/client/clientset/versioned" 10 | "github.com/pivotal/kpack/pkg/client/informers/externalversions" 11 | "github.com/pivotal/kpack/pkg/client/listers/build/v1alpha1" 12 | "github.com/rakyll/statik/fs" 13 | 14 | "github.com/matthewmcnew/kpdemo/images" 15 | "github.com/matthewmcnew/kpdemo/k8s" 16 | _ "github.com/matthewmcnew/kpdemo/statik" 17 | ) 18 | 19 | func Serve(port string) { 20 | clusterConfig, err := k8s.BuildConfigFromFlags("", "") 21 | if err != nil { 22 | log.Fatalf("Error building kubeconfig: %v", err) 23 | } 24 | 25 | client, err := versioned.NewForConfig(clusterConfig) 26 | if err != nil { 27 | log.Fatalf("could not get Build client: %s", err) 28 | } 29 | 30 | informerFactory := externalversions.NewSharedInformerFactory(client, 10*time.Hour) 31 | imageInformer := informerFactory.Kpack().V1alpha1().Images() 32 | buildInformer := informerFactory.Kpack().V1alpha1().Builds() 33 | 34 | imageLister := imageInformer.Lister() 35 | buildLister := buildInformer.Lister() 36 | 37 | stopChan := make(chan struct{}) 38 | informerFactory.Start(stopChan) 39 | 40 | statikFS, err := fs.New() 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | //fs := http.FileServer(http.Dir("ui/build")) 46 | http.Handle("/", http.FileServer(statikFS)) 47 | 48 | http.Handle("/images", &imagesApi{imageLister: imageLister, buildLister: buildLister}) 49 | log.Fatal(http.ListenAndServe(":"+port, nil)) 50 | } 51 | 52 | type imagesApi struct { 53 | imageLister v1alpha1.ImageLister 54 | buildLister v1alpha1.BuildLister 55 | } 56 | 57 | func (a *imagesApi) ServeHTTP(w http.ResponseWriter, r *http.Request) { 58 | images, err := images.Current(a.imageLister, a.buildLister) 59 | if err != nil { 60 | w.WriteHeader(http.StatusInternalServerError) 61 | return 62 | } 63 | 64 | json.NewEncoder(w).Encode(images) 65 | } 66 | -------------------------------------------------------------------------------- /setup/minikube/README.md: -------------------------------------------------------------------------------- 1 | # Minikube Setup 2 | 3 | Minikube allows you to run the entire demo on your local machine. 4 | 5 | ### Setup: 6 | 7 | Start a minikube cluster 8 | ```bash 9 | minikube start --memory=4096 --cpus=4 --vm-driver=hyperkit --bootstrapper=kubeadm --insecure-registry "registry.default.svc.cluster.local:5000" 10 | ``` 11 | 12 | Update `/etc/hosts` by adding the name registry.default.svc.cluster.local on the same line as the entry for localhost. It should look something like this: 13 | ```bash 14 | ## 15 | 127.0.0.1 localhost registry.default.svc.cluster.local 16 | 255.255.255.255 broadcasthost 17 | ::1 localhost 18 | ``` 19 | 20 | Update the minikube `/etc/hosts` with the host ip for registry.default.svc.cluster.local 21 | ```bash 22 | minikube ssh \ 23 | "echo \"192.168.64.1 registry.default.svc.cluster.local\" \ 24 | | sudo tee -a /etc/hosts" 25 | ``` 26 | 27 | Install the kubernetes dependencies 28 | 29 | ```bash 30 | kubectl apply -f https://raw.githubusercontent.com/matthewmcnew/build-service-visualization/master/setup/minikube/service.yaml 31 | ``` 32 | 33 | Make sure the registry(s) are running on your machine 34 | ```bash 35 | docker run -d -p 5000:5000 registry:2 36 | ``` 37 | 38 | Install kpack 39 | ```bash 40 | kubectl apply -f https://storage.googleapis.com/beam-releases/out.yaml 41 | ``` 42 | 43 | ### Demo: 44 | 45 | ```bash 46 | kpdemo populate --registry registry.default.svc.cluster.local:5000/please --count 15 47 | ``` 48 | -------------------------------------------------------------------------------- /setup/minikube/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | auth-registry: 4 | image: registry:2 5 | ports: 6 | - "5000:5000" -------------------------------------------------------------------------------- /setup/minikube/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | name: registry 6 | spec: 7 | ports: 8 | - protocol: TCP 9 | name: noauth 10 | port: 5001 11 | targetPort: 5001 12 | - protocol: TCP 13 | name: auth 14 | port: 5000 15 | targetPort: 5000 16 | --- 17 | kind: Endpoints 18 | apiVersion: v1 19 | metadata: 20 | name: registry 21 | subsets: 22 | - addresses: 23 | - ip: 192.168.64.1 24 | ports: 25 | - port: 5001 26 | name: noauth 27 | - port: 5000 28 | name: auth 29 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bsv", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:8081", 6 | "dependencies": { 7 | "bootstrap": "^4.3.1", 8 | "react": "^16.9.0", 9 | "react-bootstrap": "^1.0.0-beta.12", 10 | "react-copy-to-clipboard": "^5.0.1", 11 | "react-dom": "^16.9.0", 12 | "react-scripts": "^3.4.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": "react-app" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 24 | kpack Visualization 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /ui/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | } 8 | 9 | .App-header { 10 | background-color: #282c34; 11 | min-height: 100vh; 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | justify-content: center; 16 | font-size: calc(10px + 2vmin); 17 | color: white; 18 | } 19 | 20 | .App-link { 21 | color: #09d3ac; 22 | } 23 | 24 | .not-vulnerable { 25 | font-size: 50%; 26 | font-weight: 400; 27 | } 28 | 29 | .vulnerable { 30 | font-size: 100%; 31 | font-weight: 400; 32 | } 33 | 34 | .buildpack-divider { 35 | height:5pt; visibility:hidden; 36 | } 37 | 38 | .team-name { 39 | font-size: 0.5rem; 40 | margin-bottom: 0.5rem; 41 | margin-top: -0.6rem; 42 | } 43 | 44 | .modal-link { 45 | cursor: pointer; 46 | } 47 | 48 | .modal-link:hover { 49 | text-decoration: underline; 50 | } -------------------------------------------------------------------------------- /ui/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import 'bootstrap/dist/css/bootstrap.css'; 4 | import {fetchImages} from "./images"; 5 | import AppInfo from "./AppInfo"; 6 | import {ConfigModal} from "./Modal"; 7 | 8 | 9 | class AppCard extends React.Component { 10 | buildMetadata; 11 | 12 | render() { 13 | return ( 14 |
15 |
18 |
19 | 20 | 21 | {this.runImage()} 22 | 23 | {this.buildpacks().map((item, i) => 24 |
{item.id}:{item.version}
26 |
27 | )} 28 | 29 | {this.spinner()} 30 |
31 |
32 |
); 33 | } 34 | 35 | color() { 36 | if (this.props.lastBuildStatus === "True") { 37 | return "bg-success" 38 | } else if (this.props.lastBuildStatus === "False") { 39 | return "bg-danger" 40 | } 41 | 42 | return "bg-secondary" 43 | } 44 | 45 | spinner() { 46 | if (this.props.status !== "Unknown") { 47 | return null; 48 | } 49 | 50 | return ( 51 |
52 |
53 |
54 | Building... 55 |
56 |   {this.percent()} 57 |
58 | ); 59 | } 60 | 61 | percent() { 62 | if (this.props.remaining < 0) { 63 | return "Queued" 64 | } 65 | 66 | return ((this.props.completed / this.props.remaining) * 100).toFixed(0) + "%" 67 | } 68 | 69 | buildpacks() { 70 | if (this.props.buildMetadata == null) { 71 | return [] 72 | } 73 | return this.props.buildMetadata 74 | } 75 | 76 | danger(items, runImage) { 77 | if (runImage !== undefined && this.props.vulnerable.runImage !== "" && runImage.includes(this.props.vulnerable.runImage)) { 78 | return true; 79 | } 80 | for (let i = 0; i < items.length; i++) { 81 | if (items[i].id === this.props.vulnerable.buildpack && items[i].version === this.props.vulnerable.version) { 82 | return true 83 | } 84 | 85 | } 86 | return false 87 | } 88 | 89 | runImage() { 90 | if (this.props.runImage === "") { 91 | return null 92 | } 93 | 94 | const runImageParts = this.props.runImage.replace('index.docker.io/', '').split("@") 95 | 96 | if (2 !== runImageParts.length) { 97 | return null 98 | } 99 | 100 | const vulnerable = this.danger([], this.props.runImage); 101 | 102 | return ( 103 | <> 104 |
105 | Stack:{runImageParts[0].substring(0, 20)} {runImageParts[1].substr(7, 6)}
106 |
107 |
108 | 109 | ); 110 | } 111 | } 112 | 113 | class AppList extends React.Component { 114 | render() { 115 | return ( 116 |
117 | {this.props.apps.map((app, i) => )} 118 |
119 | ); 120 | } 121 | } 122 | 123 | 124 | class App extends React.Component { 125 | constructor(props) { 126 | super(props); 127 | this.state = { 128 | images: [], 129 | vulnerable: {} 130 | } 131 | } 132 | 133 | componentDidMount() { 134 | setInterval(() => fetchImages() 135 | .then(res => this.setState({ 136 | images: res, 137 | vulnerable: this.state.vulnerable 138 | })).catch(err => console.log(err)), 1000) 139 | } 140 | 141 | render() { 142 | return ( 143 | <> 144 |
145 |
146 | this.setState({ 148 | images: this.state.images, 149 | vulnerable: vulnerable 150 | })} 151 | vulnerable={this.state.vulnerable}/> 152 |
153 | 154 |
155 | 156 | ); 157 | } 158 | } 159 | 160 | export default App; 161 | -------------------------------------------------------------------------------- /ui/src/AppInfo.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react"; 2 | import {CopyToClipboard} from 'react-copy-to-clipboard'; 3 | import {Button, Modal} from 'react-bootstrap'; 4 | 5 | export default function AppInfo(props) { 6 | const [show, setShow] = useState(false); 7 | 8 | const handleClose = () => setShow(false); 9 | const handleShow = () => setShow(true); 10 | 11 | let buildpacks = props.buildMetadata; 12 | if (props.buildMetadata == null) { 13 | buildpacks = []; 14 | } 15 | 16 | let header; 17 | if (props.status === "True") { 18 | header =
{props.name.substring(0, 25)}
19 | } else { 20 | header =
{props.name.substring(0, 25)}
21 | } 22 | 23 | return ( 24 | <> 25 | 26 | {header} 27 |
28 | Team:{props.namespace} 29 |
30 | 31 | 32 | 33 | Image: {props.namespace}/{props.name} 34 | 35 | 36 |
Buildpacks
37 |
38 | {buildpacks.map((item, i) => 39 |
{item.id}:{item.version}
40 |
41 | )} 42 |
43 |
44 |
Run Image
45 |
46 |
47 | 49 | 50 | 51 | 52 | 56 | 57 |
58 |
59 | 60 | 61 |
62 |
Latest Image
63 |
64 |
65 | 67 | 68 | 69 | 70 | 74 | 75 |
76 |
77 |
78 | 79 | 80 |
81 | 82 | 85 | 86 |
87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /ui/src/Modal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Button, Form, Modal} from 'react-bootstrap'; 3 | 4 | export class ConfigModal extends React.Component { 5 | 6 | constructor(props, context) { 7 | super(props, context); 8 | this.state = { 9 | show: false 10 | } 11 | } 12 | 13 | render() { 14 | const handleClose = () => this.setState({show: false}); 15 | const handleShow = () => this.setState({show: true}); 16 | 17 | const handleSubmit = (e) => { 18 | e.preventDefault(); 19 | 20 | const buildpack = e.currentTarget.elements['buildpack'].value; 21 | const version = e.currentTarget.elements['version'].value; 22 | const runImage = e.currentTarget.elements['runImage'].value; 23 | 24 | this.props.setVulnerable({buildpack, version, runImage}); 25 | 26 | handleClose(); 27 | }; 28 | 29 | return ( 30 | <> 31 |
32 | 35 |
36 | 37 | 38 |
39 | 40 | Mark Dependency as Vulnerable 41 | 42 | 43 | 44 | 45 | Stack (Run Image) 46 | 48 | 49 | 50 | 51 |
52 | 53 | 54 | Buildpack ID 55 | 57 | 58 | 59 | 60 | Buildpack Version 61 | 63 | 64 | 65 | 66 | 67 | 70 | 73 | 74 |
75 |
76 | 77 | ); 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /ui/src/images.js: -------------------------------------------------------------------------------- 1 | export async function fetchImages() { 2 | const response = await fetch('/images'); 3 | const body = await response.json(); 4 | if (response.status !== 200) throw Error(body.message); 5 | 6 | body.sort(sort); 7 | 8 | return body; 9 | } 10 | 11 | 12 | function sort(a, b) { 13 | const aCreatedAt = Date.parse(a.createdAt); 14 | const bCreatedAt = Date.parse(b.createdAt); 15 | 16 | if (aCreatedAt === bCreatedAt) { 17 | return nameSort(a, b); 18 | } 19 | 20 | if (aCreatedAt < bCreatedAt) { 21 | return -1; 22 | } 23 | if (aCreatedAt > bCreatedAt) { 24 | return 1; 25 | } 26 | return 0; 27 | } 28 | 29 | function nameSort(a, b) { 30 | if (a.name < b.name) { 31 | return -1; 32 | } 33 | if (a.name > b.name) { 34 | return 1; 35 | } 36 | return 0; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'bootstrap/dist/css/bootstrap.css'; 4 | import './index.css'; 5 | import App from './App'; 6 | import * as serviceWorker from './serviceWorker'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: https://bit.ly/CRA-PWA 13 | serviceWorker.unregister(); 14 | -------------------------------------------------------------------------------- /ui/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------