Commit a0bc5106 authored by Matt Butcher's avatar Matt Butcher Committed by GitHub

Merge pull request #955 from technosophos/feat/932-disable-hooks

feat(helm): add --no-hook to helm install and delete
parents f171d303 1ff5499b
......@@ -175,6 +175,9 @@ message InstallReleaseRequest {
// namespace, otherwise the server will return an error. If it is not
// supplied, the server will autogenerate one.
string name = 4;
// DisableHooks causes the server to skip running any hooks for the install.
bool disable_hooks = 5;
}
// InstallReleaseResponse is the response from a release installation.
......@@ -186,6 +189,8 @@ message InstallReleaseResponse {
message UninstallReleaseRequest {
// Name is the name of the release to delete.
string name = 1;
// DisableHooks causes the server to skip running any hooks for the uninstall.
bool disable_hooks = 2;
}
// UninstallReleaseResponse represents a successful response to an uninstall request.
......
......@@ -18,6 +18,7 @@ package main
import (
"errors"
"io"
"github.com/spf13/cobra"
......@@ -32,32 +33,49 @@ Use the '--dry-run' flag to see which releases will be deleted without actually
deleting them.
`
var deleteDryRun bool
type deleteCmd struct {
name string
dryRun bool
disableHooks bool
var deleteCommand = &cobra.Command{
Use: "delete [flags] RELEASE_NAME",
Aliases: []string{"del"},
SuggestFor: []string{"remove", "rm"},
Short: "given a release name, delete the release from Kubernetes",
Long: deleteDesc,
RunE: delRelease,
PersistentPreRunE: setupConnection,
out io.Writer
client helm.Interface
}
func init() {
RootCommand.AddCommand(deleteCommand)
deleteCommand.Flags().BoolVar(&deleteDryRun, "dry-run", false, "Simulate action, but don't actually do it.")
}
func delRelease(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("command 'delete' requires a release name")
func newDeleteCmd(c helm.Interface, out io.Writer) *cobra.Command {
del := &deleteCmd{
out: out,
client: c,
}
_, err := helm.UninstallRelease(args[0], deleteDryRun)
if err != nil {
return prettyError(err)
cmd := &cobra.Command{
Use: "delete [flags] RELEASE_NAME",
Aliases: []string{"del"},
SuggestFor: []string{"remove", "rm"},
Short: "given a release name, delete the release from Kubernetes",
Long: deleteDesc,
PersistentPreRunE: setupConnection,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("command 'delete' requires a release name")
}
del.name = args[0]
del.client = ensureHelmClient(del.client)
return del.run()
},
}
f := cmd.Flags()
f.BoolVar(&del.dryRun, "dry-run", false, "simulate a delete")
f.BoolVar(&del.disableHooks, "no-hooks", false, "prevent hooks from running during deletion")
return nil
return cmd
}
func (d *deleteCmd) run() error {
opts := []helm.DeleteOption{
helm.DeleteDryRun(d.dryRun),
helm.DeleteDisableHooks(d.disableHooks),
}
_, err := d.client.DeleteRelease(d.name, opts...)
return prettyError(err)
}
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"testing"
"github.com/spf13/cobra"
)
func TestDelete(t *testing.T) {
tests := []releaseCase{
{
name: "basic delete",
args: []string{"aeneas"},
flags: []string{},
expected: "", // Output of a delete is an empty string and exit 0.
resp: releaseMock("aeneas"),
},
{
name: "delete without hooks",
args: []string{"aeneas"},
flags: []string{"--no-hooks"},
expected: "",
resp: releaseMock("aeneas"),
},
{
name: "delete without release",
args: []string{},
err: true,
},
}
runReleaseCases(t, tests, func(c *fakeReleaseClient, out io.Writer) *cobra.Command {
return newDeleteCmd(c, out)
})
}
......@@ -66,7 +66,7 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command {
}
get.release = args[0]
if get.client == nil {
get.client = helm.NewClient(helm.Host(helm.Config.ServAddr))
get.client = helm.NewClient(helm.Host(tillerHost))
}
return get.run()
},
......@@ -130,5 +130,5 @@ func ensureHelmClient(h helm.Interface) helm.Interface {
if h != nil {
return h
}
return helm.NewClient(helm.Host(helm.Config.ServAddr))
return helm.NewClient(helm.Host(tillerHost))
}
......@@ -54,7 +54,7 @@ func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command {
}
get.release = args[0]
if get.client == nil {
get.client = helm.NewClient(helm.Host(helm.Config.ServAddr))
get.client = helm.NewClient(helm.Host(tillerHost))
}
return get.run()
},
......
......@@ -25,8 +25,6 @@ import (
"github.com/spf13/cobra"
"google.golang.org/grpc"
"k8s.io/helm/pkg/helm"
)
const (
......@@ -90,6 +88,8 @@ func newRootCmd(out io.Writer) *cobra.Command {
newGetCmd(nil, out),
newListCmd(nil, out),
newStatusCmd(nil, out),
newInstallCmd(nil, out),
newDeleteCmd(nil, out),
)
return cmd
}
......@@ -118,9 +118,8 @@ func setupConnection(c *cobra.Command, args []string) error {
}
// Set up the gRPC config.
helm.Config.ServAddr = tillerHost
if flagDebug {
fmt.Printf("Server: %q\n", helm.Config.ServAddr)
fmt.Printf("Server: %q\n", tillerHost)
}
return nil
}
......@@ -153,6 +152,9 @@ func requireInit(cmd *cobra.Command, args []string) error {
// prettyError unwraps or rewrites certain errors to make them more user-friendly.
func prettyError(err error) error {
if err == nil {
return nil
}
// This is ridiculous. Why is 'grpc.rpcError' not exported? The least they
// could do is throw an interface on the lib that would let us get back
// the desc. Instead, we have to pass ALL errors through this.
......
......@@ -92,7 +92,9 @@ func (c *fakeReleaseClient) ListReleases(opts ...helm.ReleaseListOption) (*rls.L
}
func (c *fakeReleaseClient) InstallRelease(chStr string, opts ...helm.InstallOption) (*rls.InstallReleaseResponse, error) {
return nil, nil
return &rls.InstallReleaseResponse{
Release: c.rels[0],
}, nil
}
func (c *fakeReleaseClient) DeleteRelease(rlsName string, opts ...helm.DeleteOption) (*rls.UninstallReleaseResponse, error) {
......@@ -116,6 +118,10 @@ func (c *fakeReleaseClient) ReleaseContent(rlsName string, opts ...helm.ContentO
return resp, c.err
}
func (c *fakeReleaseClient) Option(opt ...helm.Option) helm.Interface {
return c
}
// releaseCmd is a command that works with a fakeReleaseClient
type releaseCmd func(c *fakeReleaseClient, out io.Writer) *cobra.Command
......@@ -127,9 +133,10 @@ func runReleaseCases(t *testing.T, tests []releaseCase, rcmd releaseCmd) {
rels: []*release.Release{tt.resp},
}
cmd := rcmd(c, &buf)
cmd.ParseFlags(tt.flags)
err := cmd.RunE(cmd, tt.args)
if (err != nil) != tt.err {
t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err)
t.Errorf("%q. expected error, got '%v'", tt.name, err)
}
re := regexp.MustCompile(tt.expected)
if !re.Match(buf.Bytes()) {
......@@ -141,8 +148,9 @@ func runReleaseCases(t *testing.T, tests []releaseCase, rcmd releaseCmd) {
// releaseCase describes a test case that works with releases.
type releaseCase struct {
name string
args []string
name string
args []string
flags []string
// expected is the string to be matched. This supports regular expressions.
expected string
err bool
......
......@@ -18,6 +18,7 @@ package main
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
......@@ -38,78 +39,90 @@ path to a chart directory or the name of a
chart in the current working directory.
`
// install flags & args
var (
// installDryRun performs a dry-run install
installDryRun bool
// installValues is the filename of supplied values.
installValues string
// installRelName is the user-supplied release name.
installRelName string
)
type installCmd struct {
name string
valuesFile string
chartPath string
dryRun bool
disableHooks bool
var installCmd = &cobra.Command{
Use: "install [CHART]",
Short: "install a chart archive",
Long: installDesc,
RunE: runInstall,
PersistentPreRunE: setupConnection,
out io.Writer
client helm.Interface
}
func init() {
f := installCmd.Flags()
f.StringVarP(&installValues, "values", "f", "", "path to a values YAML file")
f.StringVarP(&installRelName, "name", "n", "", "the release name. If unspecified, it will autogenerate one for you.")
f.BoolVar(&installDryRun, "dry-run", false, "simulate an install")
func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
inst := &installCmd{
out: out,
client: c,
}
cmd := &cobra.Command{
Use: "install [CHART]",
Short: "install a chart archive",
Long: installDesc,
PersistentPreRunE: setupConnection,
RunE: func(cmd *cobra.Command, args []string) error {
if err := checkArgsLength(1, len(args), "chart name"); err != nil {
return err
}
cp, err := locateChartPath(args[0])
if err != nil {
return err
}
inst.chartPath = cp
inst.client = ensureHelmClient(inst.client)
return inst.run()
},
}
f := cmd.Flags()
f.StringVarP(&inst.valuesFile, "values", "f", "", "path to a values YAML file")
f.StringVarP(&inst.name, "name", "n", "", "the release name. If unspecified, it will autogenerate one for you.")
f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install")
f.BoolVar(&inst.disableHooks, "no-hooks", false, "prevent hooks from running during install")
RootCommand.AddCommand(installCmd)
return cmd
}
func runInstall(cmd *cobra.Command, args []string) error {
if err := checkArgsLength(1, len(args), "chart name"); err != nil {
return err
}
chartpath, err := locateChartPath(args[0])
if err != nil {
return err
}
func (i *installCmd) run() error {
if flagDebug {
fmt.Printf("Chart path: %s\n", chartpath)
fmt.Printf("Chart path: %s\n", i.chartPath)
}
rawVals, err := vals()
rawVals, err := i.vals()
if err != nil {
return err
}
res, err := helm.InstallRelease(rawVals, installRelName, chartpath, installDryRun)
res, err := i.client.InstallRelease(i.chartPath, helm.ValueOverrides(rawVals), helm.ReleaseName(i.name), helm.InstallDryRun(i.dryRun), helm.InstallDisableHooks(i.disableHooks))
if err != nil {
return prettyError(err)
}
printRelease(res.GetRelease())
i.printRelease(res.GetRelease())
return nil
}
func vals() ([]byte, error) {
if installValues == "" {
func (i *installCmd) vals() ([]byte, error) {
if i.valuesFile == "" {
return []byte{}, nil
}
return ioutil.ReadFile(installValues)
return ioutil.ReadFile(i.valuesFile)
}
func printRelease(rel *release.Release) {
func (i *installCmd) printRelease(rel *release.Release) {
if rel == nil {
return
}
// TODO: Switch to text/template like everything else.
if flagDebug {
fmt.Printf("NAME: %s\n", rel.Name)
fmt.Printf("INFO: %s %s\n", timeconv.String(rel.Info.LastDeployed), rel.Info.Status)
fmt.Printf("CHART: %s %s\n", rel.Chart.Metadata.Name, rel.Chart.Metadata.Version)
fmt.Printf("MANIFEST: %s\n", rel.Manifest)
fmt.Fprintf(i.out, "NAME: %s\n", rel.Name)
fmt.Fprintf(i.out, "INFO: %s %s\n", timeconv.String(rel.Info.LastDeployed), rel.Info.Status)
fmt.Fprintf(i.out, "CHART: %s %s\n", rel.Chart.Metadata.Name, rel.Chart.Metadata.Version)
fmt.Fprintf(i.out, "MANIFEST: %s\n", rel.Manifest)
} else {
fmt.Println(rel.Name)
fmt.Fprintln(i.out, rel.Name)
}
}
......
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"strings"
"testing"
"github.com/spf13/cobra"
)
func TestInstall(t *testing.T) {
tests := []releaseCase{
// Install, base case
{
name: "basic install",
args: []string{"testdata/testcharts/alpine"},
flags: strings.Split("--name aeneas", " "),
expected: "aeneas",
resp: releaseMock("aeneas"),
},
// Install, no hooks
{
name: "install without hooks",
args: []string{"testdata/testcharts/alpine"},
flags: strings.Split("--name aeneas --no-hooks", " "),
expected: "juno",
resp: releaseMock("juno"),
},
// Install, no charts
{
name: "install with no chart specified",
args: []string{},
err: true,
},
}
runReleaseCases(t, tests, func(c *fakeReleaseClient, out io.Writer) *cobra.Command {
return newInstallCmd(c, out)
})
}
......@@ -80,7 +80,7 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
list.filter = strings.Join(args, " ")
}
if list.client == nil {
list.client = helm.NewClient(helm.Host(helm.Config.ServAddr))
list.client = helm.NewClient(helm.Host(tillerHost))
}
return list.run()
},
......
......@@ -52,7 +52,7 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command {
}
status.release = args[0]
if status.client == nil {
status.client = helm.NewClient(helm.Host(helm.Config.ServAddr))
status.client = helm.NewClient(helm.Host(tillerHost))
}
return status.run()
},
......
name: alpine
description: Deploy a basic Alpine Linux pod
version: 0.1.0
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm
#Alpine: A simple Helm chart
Run a single pod of Alpine Linux.
This example was generated using the command `helm create alpine`.
The `templates/` directory contains a very simple pod resource with a
couple of parameters.
The `values.yaml` file contains the default values for the
`alpine-pod.yaml` template.
You can install this example using `helm install docs/examples/alpine`.
apiVersion: v1
kind: Pod
metadata:
name: "{{.Release.Name}}-{{.Values.Name}}"
labels:
# The "heritage" label is used to track which tool deployed a given chart.
# It is useful for admins who want to see what releases a particular tool
# is responsible for.
heritage: {{.Release.Service | quote }}
# The "release" convention makes it easy to tie a release to all of the
# Kubernetes resources that were created as part of that release.
release: {{.Release.Name | quote }}
# This makes it easy to audit chart usage.
chart: "{{.Chart.Name}}-{{.Chart.Version}}"
annotations:
"helm.sh/created": {{.Release.Time.Seconds | quote }}
spec:
# This shows how to use a simple value. This will look for a passed-in value
# called restartPolicy. If it is not found, it will use the default value.
# {{default "Never" .restartPolicy}} is a slightly optimized version of the
# more conventional syntax: {{.restartPolicy | default "Never"}}
restartPolicy: {{default "Never" .Values.restartPolicy}}
containers:
- name: waiter
image: "alpine:3.3"
command: ["/bin/sleep","9000"]
......@@ -292,8 +292,10 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install
}
// pre-install hooks
if err := s.execHook(r.Hooks, r.Name, preInstall); err != nil {
return res, err
if !req.DisableHooks {
if err := s.execHook(r.Hooks, r.Name, preInstall); err != nil {
return res, err
}
}
// regular manifests
......@@ -309,8 +311,10 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install
}
// post-install hooks
if err := s.execHook(r.Hooks, r.Name, postInstall); err != nil {
return res, err
if !req.DisableHooks {
if err := s.execHook(r.Hooks, r.Name, postInstall); err != nil {
return res, err
}
}
// This is a tricky case. The release has been created, but the result
......@@ -382,8 +386,10 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
rel.Info.Deleted = timeconv.Now()
res := &services.UninstallReleaseResponse{Release: rel}
if err := s.execHook(rel.Hooks, rel.Name, preDelete); err != nil {
return res, err
if !req.DisableHooks {
if err := s.execHook(rel.Hooks, rel.Name, preDelete); err != nil {
return res, err
}
}
b := bytes.NewBuffer([]byte(rel.Manifest))
......@@ -392,8 +398,10 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
return nil, err
}
if err := s.execHook(rel.Hooks, rel.Name, postDelete); err != nil {
return res, err
if !req.DisableHooks {
if err := s.execHook(rel.Hooks, rel.Name, postDelete); err != nil {
return res, err
}
}
if err := s.env.Releases.Update(rel); err != nil {
......
......@@ -31,7 +31,6 @@ import (
"k8s.io/helm/pkg/proto/hapi/release"
"k8s.io/helm/pkg/proto/hapi/services"
"k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/timeconv"
)
var manifestWithHook = `apiVersion: v1
......@@ -50,7 +49,27 @@ func rsFixture() *releaseServer {
}
}
func releaseMock() *release.Release {
// chartStub creates a fully stubbed out chart.
func chartStub() *chart.Chart {
return &chart.Chart{
// TODO: This should be more complete.
Metadata: &chart.Metadata{
Name: "hello",
},
// This adds basic templates, partials, and hooks.
Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")},
{Name: "goodbye", Data: []byte("goodbye: world")},
{Name: "empty", Data: []byte("")},
{Name: "with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
{Name: "hooks", Data: []byte(manifestWithHook)},
},
}
}
// releaseStub creates a release stub, complete with the chartStub as its chart.
func releaseStub() *release.Release {
date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0}
return &release.Release{
Name: "angry-panda",
......@@ -59,15 +78,7 @@ func releaseMock() *release.Release {
LastDeployed: &date,
Status: &release.Status{Code: release.Status_DEPLOYED},
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "foo",
Version: "0.1.0-beta.1",
},
Templates: []*chart.Template{
{Name: "foo.tpl", Data: []byte("Hello")},
},
},
Chart: chartStub(),
Config: &chart.Config{Raw: `name = "value"`},
Hooks: []*release.Hook{
{
......@@ -88,14 +99,9 @@ func TestInstallRelease(t *testing.T) {
c := context.Background()
rs := rsFixture()
// TODO: Refactor this into a mock.
req := &services.InstallReleaseRequest{
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")},
{Name: "hooks", Data: []byte(manifestWithHook)},
},
},
Chart: chartStub(),
}
res, err := rs.InstallRelease(c, req)
if err != nil {
......@@ -144,17 +150,7 @@ func TestInstallReleaseDryRun(t *testing.T) {
rs := rsFixture()
req := &services.InstallReleaseRequest{
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")},
{Name: "goodbye", Data: []byte("goodbye: world")},
{Name: "empty", Data: []byte("")},
{Name: "with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
{Name: "hooks", Data: []byte(manifestWithHook)},
},
},
Chart: chartStub(),
DryRun: true,
}
res, err := rs.InstallRelease(c, req)
......@@ -198,30 +194,29 @@ func TestInstallReleaseDryRun(t *testing.T) {
}
}
func TestInstallReleaseNoHooks(t *testing.T) {
c := context.Background()
rs := rsFixture()
rs.env.Releases.Create(releaseStub())
req := &services.InstallReleaseRequest{
Chart: chartStub(),
DisableHooks: true,
}
res, err := rs.InstallRelease(c, req)
if err != nil {
t.Errorf("Failed install: %s", err)
}
if hl := res.Release.Hooks[0].LastRun; hl != nil {
t.Errorf("Expected that no hooks were run. Got %d", hl)
}
}
func TestUninstallRelease(t *testing.T) {
c := context.Background()
rs := rsFixture()
rs.env.Releases.Create(&release.Release{
Name: "angry-panda",
Info: &release.Info{
FirstDeployed: timeconv.Now(),
Status: &release.Status{
Code: release.Status_DEPLOYED,
},
},
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.Hook_Event{
release.Hook_POST_INSTALL,
release.Hook_PRE_DELETE,
},
},
},
})
rs.env.Releases.Create(releaseStub())
req := &services.UninstallReleaseRequest{
Name: "angry-panda",
......@@ -249,10 +244,31 @@ func TestUninstallRelease(t *testing.T) {
}
}
func TestUninstallReleaseNoHooks(t *testing.T) {
c := context.Background()
rs := rsFixture()
rs.env.Releases.Create(releaseStub())
req := &services.UninstallReleaseRequest{
Name: "angry-panda",
DisableHooks: true,
}
res, err := rs.UninstallRelease(c, req)
if err != nil {
t.Errorf("Failed uninstall: %s", err)
}
// The default value for a protobuf timestamp is nil.
if res.Release.Hooks[0].LastRun != nil {
t.Errorf("Expected LastRun to be zero, got %d.", res.Release.Hooks[0].LastRun.Seconds)
}
}
func TestGetReleaseContent(t *testing.T) {
c := context.Background()
rs := rsFixture()
rel := releaseMock()
rel := releaseStub()
if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
}
......@@ -270,7 +286,7 @@ func TestGetReleaseContent(t *testing.T) {
func TestGetReleaseStatus(t *testing.T) {
c := context.Background()
rs := rsFixture()
rel := releaseMock()
rel := releaseStub()
if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
}
......@@ -289,7 +305,7 @@ func TestListReleases(t *testing.T) {
rs := rsFixture()
num := 7
for i := 0; i < num; i++ {
rel := releaseMock()
rel := releaseStub()
rel.Name = fmt.Sprintf("rel-%d", i)
if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
......@@ -313,7 +329,7 @@ func TestListReleasesSort(t *testing.T) {
// sort.
num := 7
for i := num; i > 0; i-- {
rel := releaseMock()
rel := releaseStub()
rel.Name = fmt.Sprintf("rel-%d", i)
if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
......@@ -356,7 +372,7 @@ func TestListReleasesFilter(t *testing.T) {
}
num := 7
for i := 0; i < num; i++ {
rel := releaseMock()
rel := releaseStub()
rel.Name = names[i]
if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
......
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package helm
import (
rls "k8s.io/helm/pkg/proto/hapi/services"
)
// These APIs are a temporary abstraction layer that captures the interaction between the current cmd/helm and old
// pkg/helm implementations. Post refactor the cmd/helm package will use the APIs exposed on helm.Client directly.
// Config is the base configuration
var Config struct {
ServAddr string
}
// ListReleases lists releases. DEPRECATED.
//
// Soon to be deprecated helm ListReleases API.
func ListReleases(limit int, offset string, sort rls.ListSort_SortBy, order rls.ListSort_SortOrder, filter string) (*rls.ListReleasesResponse, error) {
opts := []ReleaseListOption{
ReleaseListLimit(limit),
ReleaseListOffset(offset),
ReleaseListFilter(filter),
ReleaseListSort(int32(sort)),
ReleaseListOrder(int32(order)),
}
return NewClient(Host(Config.ServAddr)).ListReleases(opts...)
}
// GetReleaseStatus gets a release status. DEPRECATED
//
// Soon to be deprecated helm GetReleaseStatus API.
func GetReleaseStatus(rlsName string) (*rls.GetReleaseStatusResponse, error) {
return NewClient(Host(Config.ServAddr)).ReleaseStatus(rlsName)
}
// GetReleaseContent gets the content of a release.
// Soon to be deprecated helm GetReleaseContent API.
func GetReleaseContent(rlsName string) (*rls.GetReleaseContentResponse, error) {
return NewClient(Host(Config.ServAddr)).ReleaseContent(rlsName)
}
// UpdateRelease updates a release.
// Soon to be deprecated helm UpdateRelease API.
func UpdateRelease(rlsName string) (*rls.UpdateReleaseResponse, error) {
return NewClient(Host(Config.ServAddr)).UpdateRelease(rlsName)
}
// InstallRelease runs an install for a release.
// Soon to be deprecated helm InstallRelease API.
func InstallRelease(vals []byte, rlsName, chStr string, dryRun bool) (*rls.InstallReleaseResponse, error) {
client := NewClient(Host(Config.ServAddr))
if dryRun {
client.Option(DryRun())
}
return client.InstallRelease(chStr, ValueOverrides(vals), ReleaseName(rlsName))
}
// UninstallRelease destroys an existing release.
// Soon to be deprecated helm UninstallRelease API.
func UninstallRelease(rlsName string, dryRun bool) (*rls.UninstallReleaseResponse, error) {
client := NewClient(Host(Config.ServAddr))
if dryRun {
client.Option(DryRun())
}
return client.DeleteRelease(rlsName)
}
......@@ -38,19 +38,14 @@ type options struct {
chart string
// if set dry-run helm client calls
dryRun bool
// if set, skip running hooks
disableHooks bool
// release list options are applied directly to the list releases request
listReq rls.ListReleasesRequest
// release install options are applied directly to the install release request
instReq rls.InstallReleaseRequest
}
// DryRun returns an Option which instructs the helm client to dry-run tiller rpcs.
func DryRun() Option {
return func(opts *options) {
opts.dryRun = true
}
}
// Home specifies the location of helm home, (default = "$HOME/.helm").
func Home(home string) Option {
return func(opts *options) {
......@@ -124,6 +119,34 @@ func ReleaseName(name string) InstallOption {
}
}
// DeleteDisableHooks will disable hooks for a deletion operation.
func DeleteDisableHooks(disable bool) DeleteOption {
return func(opts *options) {
opts.disableHooks = disable
}
}
// DeleteDryRun will (if true) execute a deletion as a dry run.
func DeleteDryRun(dry bool) DeleteOption {
return func(opts *options) {
opts.dryRun = dry
}
}
// InstallDisableHooks disables hooks during installation.
func InstallDisableHooks(disable bool) InstallOption {
return func(opts *options) {
opts.disableHooks = disable
}
}
// InstallDryRun will (if true) execute an installation as a dry run.
func InstallDryRun(dry bool) InstallOption {
return func(opts *options) {
opts.dryRun = dry
}
}
// ContentOption -- TODO
type ContentOption func(*options)
......@@ -163,12 +186,16 @@ func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient
}
o.instReq.Chart = chr
o.instReq.DryRun = o.dryRun
o.instReq.DisableHooks = o.disableHooks
return rlc.InstallRelease(context.TODO(), &o.instReq)
}
// Executes tiller.UninstallRelease RPC.
func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) {
for _, opt := range opts {
opt(o)
}
if o.dryRun {
// In the dry run case, just see if the release exists
r, err := o.rpcGetReleaseContent(rlsName, rlc)
......@@ -177,8 +204,7 @@ func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient,
}
return &rls.UninstallReleaseResponse{Release: r.Release}, nil
}
return rlc.UninstallRelease(context.TODO(), &rls.UninstallReleaseRequest{Name: rlsName})
return rlc.UninstallRelease(context.TODO(), &rls.UninstallReleaseRequest{Name: rlsName, DisableHooks: o.disableHooks})
}
// Executes tiller.UpdateRelease RPC.
......
......@@ -248,6 +248,8 @@ type InstallReleaseRequest struct {
// namespace, otherwise the server will return an error. If it is not
// supplied, the server will autogenerate one.
Name string `protobuf:"bytes,4,opt,name=name" json:"name,omitempty"`
// DisableHooks causes the server to skip running any hooks for the install.
DisableHooks bool `protobuf:"varint,5,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"`
}
func (m *InstallReleaseRequest) Reset() { *m = InstallReleaseRequest{} }
......@@ -290,6 +292,8 @@ func (m *InstallReleaseResponse) GetRelease() *hapi_release3.Release {
type UninstallReleaseRequest struct {
// Name is the name of the release to delete.
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
// DisableHooks causes the server to skip running any hooks for the uninstall.
DisableHooks bool `protobuf:"varint,2,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"`
}
func (m *UninstallReleaseRequest) Reset() { *m = UninstallReleaseRequest{} }
......@@ -586,48 +590,50 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{
}
var fileDescriptor0 = []byte{
// 688 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x55, 0xdd, 0x4e, 0x13, 0x41,
0x14, 0x66, 0x69, 0x69, 0xcb, 0x41, 0x48, 0x39, 0x96, 0xb6, 0xee, 0x85, 0x31, 0x9b, 0xa8, 0x88,
0xb2, 0xd5, 0x7a, 0x6f, 0x52, 0xa0, 0x21, 0x84, 0x5a, 0x92, 0xa9, 0x68, 0xe2, 0x85, 0x64, 0x81,
0xa9, 0xac, 0x59, 0x76, 0xeb, 0xce, 0x94, 0xc8, 0x23, 0xf8, 0x08, 0xbe, 0x89, 0x0f, 0xe4, 0x83,
0x38, 0x3f, 0x3b, 0x9b, 0x6e, 0xd9, 0xd5, 0x86, 0x9b, 0xdd, 0x99, 0xfd, 0xbe, 0x39, 0xdf, 0x99,
0xef, 0x9c, 0xd3, 0x82, 0x7d, 0xe5, 0x4d, 0xfc, 0x0e, 0xa3, 0xf1, 0x8d, 0x7f, 0x41, 0x59, 0x87,
0xfb, 0x41, 0x40, 0x63, 0x77, 0x12, 0x47, 0x3c, 0xc2, 0x86, 0xc4, 0x5c, 0x83, 0xb9, 0x1a, 0xb3,
0x9b, 0xea, 0xc4, 0xc5, 0x95, 0x17, 0x73, 0xfd, 0xd4, 0x6c, 0xbb, 0x35, 0xfb, 0x3d, 0x0a, 0xc7,
0xfe, 0xd7, 0x04, 0xd0, 0x12, 0x31, 0x0d, 0xa8, 0xc7, 0xa8, 0x79, 0x67, 0x0e, 0x19, 0xcc, 0x0f,
0xc7, 0x91, 0x06, 0x9c, 0x3f, 0x16, 0x3c, 0x1c, 0xf8, 0x8c, 0x13, 0x0d, 0x31, 0x42, 0xbf, 0x4f,
0x29, 0xe3, 0xd8, 0x80, 0x95, 0xc0, 0xbf, 0xf6, 0x79, 0xdb, 0x7a, 0x62, 0x6d, 0x97, 0x88, 0xde,
0x60, 0x13, 0x2a, 0xd1, 0x78, 0xcc, 0x28, 0x6f, 0x2f, 0x8b, 0xcf, 0xab, 0x24, 0xd9, 0xe1, 0x3b,
0xa8, 0xb2, 0x28, 0xe6, 0x67, 0xe7, 0xb7, 0xed, 0x92, 0x00, 0x36, 0xba, 0x4f, 0xdd, 0xbc, 0x3b,
0xb9, 0x52, 0x69, 0x24, 0x88, 0xae, 0x7c, 0xec, 0xdd, 0x92, 0x0a, 0x53, 0x6f, 0x19, 0x77, 0xec,
0x07, 0x9c, 0xc6, 0xed, 0xb2, 0x8e, 0xab, 0x77, 0x78, 0x08, 0xa0, 0xe2, 0x46, 0xf1, 0xa5, 0xc0,
0x56, 0x54, 0xe8, 0xed, 0x05, 0x42, 0x9f, 0x48, 0x3e, 0x59, 0x65, 0x66, 0xe9, 0x7c, 0x81, 0x9a,
0x21, 0x38, 0x5d, 0xa8, 0x68, 0x79, 0x5c, 0x83, 0xea, 0xe9, 0xf0, 0x78, 0x78, 0xf2, 0x69, 0x58,
0x5f, 0xc2, 0x1a, 0x94, 0x87, 0xbd, 0xf7, 0xfd, 0xba, 0x85, 0x9b, 0xb0, 0x3e, 0xe8, 0x8d, 0x3e,
0x9c, 0x91, 0xfe, 0xa0, 0xdf, 0x1b, 0xf5, 0x0f, 0xea, 0xcb, 0xce, 0x63, 0x58, 0x4d, 0xe3, 0x62,
0x15, 0x4a, 0xbd, 0xd1, 0xbe, 0x3e, 0x72, 0xd0, 0x17, 0x2b, 0xcb, 0xf9, 0x69, 0x41, 0x23, 0x6b,
0x23, 0x9b, 0x44, 0x21, 0xa3, 0xd2, 0xc7, 0x8b, 0x68, 0x1a, 0xa6, 0x3e, 0xaa, 0x0d, 0x22, 0x94,
0x43, 0xfa, 0xc3, 0xb8, 0xa8, 0xd6, 0x92, 0xc9, 0x23, 0xee, 0x05, 0xca, 0x41, 0xc1, 0x54, 0x1b,
0x7c, 0x03, 0xb5, 0xa4, 0x6a, 0x4c, 0x78, 0x53, 0xda, 0x5e, 0xeb, 0x6e, 0xe9, 0xfb, 0x9b, 0xfa,
0x26, 0x8a, 0x24, 0xa5, 0x39, 0xbb, 0xd0, 0x3a, 0xa4, 0x26, 0x93, 0x11, 0xf7, 0xf8, 0x34, 0xad,
0xaa, 0xd4, 0xf5, 0xae, 0xa9, 0x4a, 0x46, 0xea, 0x8a, 0xb5, 0xf3, 0x11, 0xda, 0x77, 0xe9, 0x49,
0xf6, 0x39, 0x7c, 0x7c, 0x06, 0x65, 0xd9, 0x3f, 0x2a, 0xf7, 0xb5, 0x2e, 0x66, 0xb3, 0x39, 0x12,
0x08, 0x51, 0xb8, 0xe3, 0xce, 0xc6, 0xdd, 0x8f, 0x42, 0x4e, 0x43, 0xfe, 0xaf, 0x3c, 0x06, 0xf0,
0x28, 0x87, 0x9f, 0x24, 0xd2, 0x81, 0x6a, 0x22, 0xa1, 0xce, 0x14, 0xba, 0x60, 0x58, 0x4e, 0x13,
0x1a, 0xa7, 0x93, 0x4b, 0x8f, 0x53, 0x83, 0x68, 0x65, 0xa7, 0x05, 0x5b, 0x73, 0xdf, 0xb5, 0x82,
0xf3, 0xcb, 0x82, 0xad, 0xa3, 0x90, 0x09, 0xcf, 0x83, 0xec, 0x11, 0x7c, 0x2e, 0x4a, 0x28, 0xa7,
0x2d, 0x51, 0xde, 0xd4, 0xca, 0x7a, 0x24, 0xf7, 0xe5, 0x93, 0x68, 0x1c, 0x77, 0xa0, 0x72, 0xe3,
0x05, 0xe2, 0x4c, 0xd6, 0x9b, 0x84, 0xa9, 0x46, 0x95, 0x24, 0x0c, 0x6c, 0x41, 0xf5, 0x32, 0xbe,
0x3d, 0x8b, 0xa7, 0xa1, 0xaa, 0x77, 0x8d, 0x54, 0xc4, 0x96, 0x4c, 0xc3, 0xd4, 0x9a, 0xf2, 0x8c,
0x35, 0x47, 0xd0, 0x9c, 0x4f, 0xed, 0xbe, 0xbe, 0x88, 0xe6, 0x38, 0x0d, 0xfd, 0xdc, 0x7b, 0xe6,
0x15, 0xe5, 0x18, 0xda, 0x77, 0xe9, 0xf7, 0xd4, 0xee, 0xfe, 0x5e, 0x81, 0x0d, 0xd3, 0x67, 0x7a,
0x7a, 0xd1, 0x87, 0x07, 0xb3, 0x63, 0x83, 0x2f, 0x8a, 0x87, 0x7b, 0xee, 0x17, 0xca, 0xde, 0x59,
0x84, 0x9a, 0x14, 0x77, 0xe9, 0xb5, 0x85, 0x0c, 0xea, 0xf3, 0x7d, 0x8e, 0xbb, 0xf9, 0x31, 0x0a,
0xc6, 0xc7, 0x76, 0x17, 0xa5, 0x1b, 0x59, 0xbc, 0x81, 0xcd, 0x3b, 0x4d, 0x8d, 0xff, 0x0d, 0x93,
0x9d, 0x16, 0xbb, 0xb3, 0x30, 0x3f, 0xd5, 0xfd, 0x06, 0xeb, 0x99, 0x36, 0xc7, 0x02, 0xb7, 0xf2,
0x66, 0xc4, 0x7e, 0xb9, 0x10, 0x37, 0xd5, 0xba, 0x86, 0x8d, 0x6c, 0x77, 0x62, 0x41, 0x80, 0xdc,
0xf1, 0xb2, 0x5f, 0x2d, 0x46, 0x4e, 0xe5, 0x44, 0x1d, 0xe7, 0x5b, 0xb2, 0xa8, 0x8e, 0x05, 0x9d,
0x5e, 0x54, 0xc7, 0xa2, 0x4e, 0x77, 0x96, 0xf6, 0xe0, 0x73, 0xcd, 0xb0, 0xcf, 0x2b, 0xea, 0x9f,
0xf3, 0xed, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x39, 0x3c, 0xcd, 0x3c, 0xd3, 0x07, 0x00, 0x00,
// 720 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x55, 0xdd, 0x6e, 0xd3, 0x4a,
0x10, 0xae, 0x9b, 0x34, 0x49, 0xa7, 0x3f, 0x4a, 0xf7, 0xa4, 0x49, 0x8e, 0x2f, 0x8e, 0x8e, 0x8c,
0x80, 0x52, 0xa8, 0x03, 0xe1, 0x1e, 0x29, 0x6d, 0xa3, 0x52, 0x35, 0xa4, 0xd2, 0x86, 0x82, 0xc4,
0x05, 0x91, 0xdb, 0x6c, 0xa8, 0xc1, 0xf5, 0x06, 0xef, 0xa6, 0xa2, 0x8f, 0xc0, 0x1b, 0x71, 0xc5,
0xd3, 0xf0, 0x20, 0xec, 0x8f, 0xd7, 0x8a, 0x13, 0x1b, 0xa2, 0xde, 0x38, 0xbb, 0x3b, 0xdf, 0xce,
0x37, 0xfe, 0x66, 0x3e, 0x07, 0xec, 0x6b, 0x6f, 0xe2, 0xb7, 0x18, 0x89, 0x6e, 0xfd, 0x2b, 0xc2,
0x5a, 0xdc, 0x0f, 0x02, 0x12, 0xb9, 0x93, 0x88, 0x72, 0x8a, 0x6a, 0x32, 0xe6, 0x9a, 0x98, 0xab,
0x63, 0x76, 0x5d, 0xdd, 0xb8, 0xba, 0xf6, 0x22, 0xae, 0x9f, 0x1a, 0x6d, 0x37, 0x66, 0xcf, 0x69,
0x38, 0xf6, 0x3f, 0xc5, 0x01, 0x4d, 0x11, 0x91, 0x80, 0x78, 0x8c, 0x98, 0xdf, 0xd4, 0x25, 0x13,
0xf3, 0xc3, 0x31, 0xd5, 0x01, 0xe7, 0x97, 0x05, 0xff, 0xf4, 0x7c, 0xc6, 0xb1, 0x0e, 0x31, 0x4c,
0xbe, 0x4e, 0x09, 0xe3, 0xa8, 0x06, 0x6b, 0x81, 0x7f, 0xe3, 0xf3, 0xa6, 0xf5, 0xbf, 0xb5, 0x57,
0xc0, 0x7a, 0x83, 0xea, 0x50, 0xa2, 0xe3, 0x31, 0x23, 0xbc, 0xb9, 0x2a, 0x8e, 0xd7, 0x71, 0xbc,
0x43, 0xaf, 0xa0, 0xcc, 0x68, 0xc4, 0x87, 0x97, 0x77, 0xcd, 0x82, 0x08, 0x6c, 0xb7, 0x1f, 0xba,
0x59, 0xef, 0xe4, 0x4a, 0xa6, 0x81, 0x00, 0xba, 0xf2, 0x71, 0x78, 0x87, 0x4b, 0x4c, 0xfd, 0xca,
0xbc, 0x63, 0x3f, 0xe0, 0x24, 0x6a, 0x16, 0x75, 0x5e, 0xbd, 0x43, 0x27, 0x00, 0x2a, 0x2f, 0x8d,
0x46, 0x22, 0xb6, 0xa6, 0x52, 0xef, 0x2d, 0x91, 0xfa, 0x5c, 0xe2, 0xf1, 0x3a, 0x33, 0x4b, 0xe7,
0x23, 0x54, 0x0c, 0xc0, 0x69, 0x43, 0x49, 0xd3, 0xa3, 0x0d, 0x28, 0x5f, 0xf4, 0xcf, 0xfa, 0xe7,
0xef, 0xfb, 0xd5, 0x15, 0x54, 0x81, 0x62, 0xbf, 0xf3, 0xa6, 0x5b, 0xb5, 0xd0, 0x0e, 0x6c, 0xf5,
0x3a, 0x83, 0xb7, 0x43, 0xdc, 0xed, 0x75, 0x3b, 0x83, 0xee, 0x71, 0x75, 0xd5, 0xf9, 0x0f, 0xd6,
0x93, 0xbc, 0xa8, 0x0c, 0x85, 0xce, 0xe0, 0x48, 0x5f, 0x39, 0xee, 0x8a, 0x95, 0xe5, 0x7c, 0xb7,
0xa0, 0x96, 0x96, 0x91, 0x4d, 0x68, 0xc8, 0x88, 0xd4, 0xf1, 0x8a, 0x4e, 0xc3, 0x44, 0x47, 0xb5,
0x41, 0x08, 0x8a, 0x21, 0xf9, 0x66, 0x54, 0x54, 0x6b, 0x89, 0xe4, 0x94, 0x7b, 0x81, 0x52, 0x50,
0x20, 0xd5, 0x06, 0xbd, 0x80, 0x4a, 0xdc, 0x35, 0x26, 0xb4, 0x29, 0xec, 0x6d, 0xb4, 0x77, 0xf5,
0xfb, 0x9b, 0xfe, 0xc6, 0x8c, 0x38, 0x81, 0x39, 0x07, 0xd0, 0x38, 0x21, 0xa6, 0x92, 0x01, 0xf7,
0xf8, 0x34, 0xe9, 0xaa, 0xe4, 0xf5, 0x6e, 0x88, 0x2a, 0x46, 0xf2, 0x8a, 0xb5, 0xf3, 0x0e, 0x9a,
0x8b, 0xf0, 0xb8, 0xfa, 0x0c, 0x3c, 0x7a, 0x04, 0x45, 0x39, 0x3f, 0xaa, 0xf6, 0x8d, 0x36, 0x4a,
0x57, 0x73, 0x2a, 0x22, 0x58, 0xc5, 0x1d, 0x77, 0x36, 0xef, 0x11, 0x0d, 0x39, 0x09, 0xf9, 0x9f,
0xea, 0xe8, 0xc1, 0xbf, 0x19, 0xf8, 0xb8, 0x90, 0x16, 0x94, 0x63, 0x0a, 0x75, 0x27, 0x57, 0x05,
0x83, 0x72, 0xea, 0x50, 0xbb, 0x98, 0x8c, 0x3c, 0x4e, 0x4c, 0x44, 0x33, 0x3b, 0x0d, 0xd8, 0x9d,
0x3b, 0xd7, 0x0c, 0xce, 0x4f, 0x0b, 0x76, 0x4f, 0x43, 0x26, 0x34, 0x0f, 0xd2, 0x57, 0xd0, 0x63,
0xd1, 0x42, 0xe9, 0xb6, 0x98, 0x79, 0x47, 0x33, 0x6b, 0x4b, 0x1e, 0xc9, 0x27, 0xd6, 0x71, 0xb4,
0x0f, 0xa5, 0x5b, 0x2f, 0x10, 0x77, 0xd2, 0xda, 0xc4, 0x48, 0x65, 0x55, 0x1c, 0x23, 0x50, 0x03,
0xca, 0xa3, 0xe8, 0x6e, 0x18, 0x4d, 0x43, 0xd5, 0xef, 0x0a, 0x2e, 0x89, 0x2d, 0x9e, 0x86, 0x89,
0x34, 0xc5, 0x19, 0xc9, 0x1f, 0xc0, 0xd6, 0xc8, 0x67, 0xde, 0x65, 0x40, 0x86, 0xd7, 0x94, 0x7e,
0x61, 0xca, 0x09, 0x15, 0xbc, 0x19, 0x1f, 0xbe, 0x96, 0x67, 0xce, 0x29, 0xd4, 0xe7, 0xeb, 0xbf,
0xaf, 0x78, 0x18, 0x1a, 0x17, 0xa1, 0x9f, 0x29, 0x46, 0xd6, 0x44, 0x2c, 0x94, 0xb7, 0x9a, 0x51,
0xde, 0x19, 0x34, 0x17, 0x73, 0xde, 0xb3, 0xc0, 0xf6, 0x8f, 0x35, 0xd8, 0x36, 0x13, 0xab, 0xbf,
0x03, 0xc8, 0x87, 0xcd, 0x59, 0x03, 0xa2, 0x27, 0xf9, 0x9f, 0x89, 0xb9, 0x6f, 0x9d, 0xbd, 0xbf,
0x0c, 0x34, 0x1e, 0x93, 0x95, 0xe7, 0x16, 0x62, 0x50, 0x9d, 0x77, 0x0c, 0x3a, 0xc8, 0xce, 0x91,
0x63, 0x44, 0xdb, 0x5d, 0x16, 0x6e, 0x68, 0xd1, 0x2d, 0xec, 0x2c, 0xd8, 0x03, 0xfd, 0x35, 0x4d,
0xda, 0x77, 0x76, 0x6b, 0x69, 0x7c, 0xc2, 0xfb, 0x19, 0xb6, 0x52, 0x86, 0x41, 0x39, 0x6a, 0x65,
0xb9, 0xcd, 0x7e, 0xba, 0x14, 0x36, 0xe1, 0xba, 0x81, 0xed, 0xf4, 0x08, 0xa3, 0x9c, 0x04, 0x99,
0x46, 0xb5, 0x9f, 0x2d, 0x07, 0x4e, 0xe8, 0x44, 0x1f, 0xe7, 0x47, 0x32, 0xaf, 0x8f, 0x39, 0x76,
0xc8, 0xeb, 0x63, 0xde, 0xa4, 0x3b, 0x2b, 0x87, 0xf0, 0xa1, 0x62, 0xd0, 0x97, 0x25, 0xf5, 0x1f,
0xfc, 0xf2, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0x38, 0x16, 0x87, 0x5f, 0x1d, 0x08, 0x00, 0x00,
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment