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 { ...@@ -175,6 +175,9 @@ message InstallReleaseRequest {
// namespace, otherwise the server will return an error. If it is not // namespace, otherwise the server will return an error. If it is not
// supplied, the server will autogenerate one. // supplied, the server will autogenerate one.
string name = 4; 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. // InstallReleaseResponse is the response from a release installation.
...@@ -186,6 +189,8 @@ message InstallReleaseResponse { ...@@ -186,6 +189,8 @@ message InstallReleaseResponse {
message UninstallReleaseRequest { message UninstallReleaseRequest {
// Name is the name of the release to delete. // Name is the name of the release to delete.
string name = 1; 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. // UninstallReleaseResponse represents a successful response to an uninstall request.
......
...@@ -18,6 +18,7 @@ package main ...@@ -18,6 +18,7 @@ package main
import ( import (
"errors" "errors"
"io"
"github.com/spf13/cobra" "github.com/spf13/cobra"
...@@ -32,32 +33,49 @@ Use the '--dry-run' flag to see which releases will be deleted without actually ...@@ -32,32 +33,49 @@ Use the '--dry-run' flag to see which releases will be deleted without actually
deleting them. deleting them.
` `
var deleteDryRun bool type deleteCmd struct {
name string
dryRun bool
disableHooks bool
var deleteCommand = &cobra.Command{ out io.Writer
client helm.Interface
}
func newDeleteCmd(c helm.Interface, out io.Writer) *cobra.Command {
del := &deleteCmd{
out: out,
client: c,
}
cmd := &cobra.Command{
Use: "delete [flags] RELEASE_NAME", Use: "delete [flags] RELEASE_NAME",
Aliases: []string{"del"}, Aliases: []string{"del"},
SuggestFor: []string{"remove", "rm"}, SuggestFor: []string{"remove", "rm"},
Short: "given a release name, delete the release from Kubernetes", Short: "given a release name, delete the release from Kubernetes",
Long: deleteDesc, Long: deleteDesc,
RunE: delRelease,
PersistentPreRunE: setupConnection, PersistentPreRunE: setupConnection,
} RunE: func(cmd *cobra.Command, args []string) error {
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 { if len(args) == 0 {
return errors.New("command 'delete' requires a release name") return errors.New("command 'delete' requires a release name")
} }
del.name = args[0]
_, err := helm.UninstallRelease(args[0], deleteDryRun) del.client = ensureHelmClient(del.client)
if err != nil { return del.run()
return prettyError(err) },
} }
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 { ...@@ -66,7 +66,7 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command {
} }
get.release = args[0] get.release = args[0]
if get.client == nil { if get.client == nil {
get.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) get.client = helm.NewClient(helm.Host(tillerHost))
} }
return get.run() return get.run()
}, },
...@@ -130,5 +130,5 @@ func ensureHelmClient(h helm.Interface) helm.Interface { ...@@ -130,5 +130,5 @@ func ensureHelmClient(h helm.Interface) helm.Interface {
if h != nil { if h != nil {
return h 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 { ...@@ -54,7 +54,7 @@ func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command {
} }
get.release = args[0] get.release = args[0]
if get.client == nil { if get.client == nil {
get.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) get.client = helm.NewClient(helm.Host(tillerHost))
} }
return get.run() return get.run()
}, },
......
...@@ -25,8 +25,6 @@ import ( ...@@ -25,8 +25,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc" "google.golang.org/grpc"
"k8s.io/helm/pkg/helm"
) )
const ( const (
...@@ -90,6 +88,8 @@ func newRootCmd(out io.Writer) *cobra.Command { ...@@ -90,6 +88,8 @@ func newRootCmd(out io.Writer) *cobra.Command {
newGetCmd(nil, out), newGetCmd(nil, out),
newListCmd(nil, out), newListCmd(nil, out),
newStatusCmd(nil, out), newStatusCmd(nil, out),
newInstallCmd(nil, out),
newDeleteCmd(nil, out),
) )
return cmd return cmd
} }
...@@ -118,9 +118,8 @@ func setupConnection(c *cobra.Command, args []string) error { ...@@ -118,9 +118,8 @@ func setupConnection(c *cobra.Command, args []string) error {
} }
// Set up the gRPC config. // Set up the gRPC config.
helm.Config.ServAddr = tillerHost
if flagDebug { if flagDebug {
fmt.Printf("Server: %q\n", helm.Config.ServAddr) fmt.Printf("Server: %q\n", tillerHost)
} }
return nil return nil
} }
...@@ -153,6 +152,9 @@ func requireInit(cmd *cobra.Command, args []string) error { ...@@ -153,6 +152,9 @@ func requireInit(cmd *cobra.Command, args []string) error {
// prettyError unwraps or rewrites certain errors to make them more user-friendly. // prettyError unwraps or rewrites certain errors to make them more user-friendly.
func prettyError(err error) error { func prettyError(err error) error {
if err == nil {
return nil
}
// This is ridiculous. Why is 'grpc.rpcError' not exported? The least they // 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 // 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. // the desc. Instead, we have to pass ALL errors through this.
......
...@@ -92,7 +92,9 @@ func (c *fakeReleaseClient) ListReleases(opts ...helm.ReleaseListOption) (*rls.L ...@@ -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) { 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) { 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 ...@@ -116,6 +118,10 @@ func (c *fakeReleaseClient) ReleaseContent(rlsName string, opts ...helm.ContentO
return resp, c.err return resp, c.err
} }
func (c *fakeReleaseClient) Option(opt ...helm.Option) helm.Interface {
return c
}
// releaseCmd is a command that works with a fakeReleaseClient // releaseCmd is a command that works with a fakeReleaseClient
type releaseCmd func(c *fakeReleaseClient, out io.Writer) *cobra.Command type releaseCmd func(c *fakeReleaseClient, out io.Writer) *cobra.Command
...@@ -127,9 +133,10 @@ func runReleaseCases(t *testing.T, tests []releaseCase, rcmd releaseCmd) { ...@@ -127,9 +133,10 @@ func runReleaseCases(t *testing.T, tests []releaseCase, rcmd releaseCmd) {
rels: []*release.Release{tt.resp}, rels: []*release.Release{tt.resp},
} }
cmd := rcmd(c, &buf) cmd := rcmd(c, &buf)
cmd.ParseFlags(tt.flags)
err := cmd.RunE(cmd, tt.args) err := cmd.RunE(cmd, tt.args)
if (err != nil) != tt.err { 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) re := regexp.MustCompile(tt.expected)
if !re.Match(buf.Bytes()) { if !re.Match(buf.Bytes()) {
...@@ -143,6 +150,7 @@ func runReleaseCases(t *testing.T, tests []releaseCase, rcmd releaseCmd) { ...@@ -143,6 +150,7 @@ func runReleaseCases(t *testing.T, tests []releaseCase, rcmd releaseCmd) {
type releaseCase struct { type releaseCase struct {
name string name string
args []string args []string
flags []string
// expected is the string to be matched. This supports regular expressions. // expected is the string to be matched. This supports regular expressions.
expected string expected string
err bool err bool
......
...@@ -18,6 +18,7 @@ package main ...@@ -18,6 +18,7 @@ package main
import ( import (
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
...@@ -38,78 +39,90 @@ path to a chart directory or the name of a ...@@ -38,78 +39,90 @@ path to a chart directory or the name of a
chart in the current working directory. chart in the current working directory.
` `
// install flags & args type installCmd struct {
var ( name string
// installDryRun performs a dry-run install valuesFile string
installDryRun bool chartPath string
// installValues is the filename of supplied values. dryRun bool
installValues string disableHooks bool
// installRelName is the user-supplied release name.
installRelName string out io.Writer
) client helm.Interface
}
func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
inst := &installCmd{
out: out,
client: c,
}
var installCmd = &cobra.Command{ cmd := &cobra.Command{
Use: "install [CHART]", Use: "install [CHART]",
Short: "install a chart archive", Short: "install a chart archive",
Long: installDesc, Long: installDesc,
RunE: runInstall,
PersistentPreRunE: setupConnection, PersistentPreRunE: setupConnection,
} RunE: func(cmd *cobra.Command, args []string) error {
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")
RootCommand.AddCommand(installCmd)
}
func runInstall(cmd *cobra.Command, args []string) error {
if err := checkArgsLength(1, len(args), "chart name"); err != nil { if err := checkArgsLength(1, len(args), "chart name"); err != nil {
return err return err
} }
chartpath, err := locateChartPath(args[0]) cp, err := locateChartPath(args[0])
if err != nil { if err != nil {
return err 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")
return cmd
}
func (i *installCmd) run() error {
if flagDebug { 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 { if err != nil {
return err 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 { if err != nil {
return prettyError(err) return prettyError(err)
} }
printRelease(res.GetRelease()) i.printRelease(res.GetRelease())
return nil return nil
} }
func vals() ([]byte, error) { func (i *installCmd) vals() ([]byte, error) {
if installValues == "" { if i.valuesFile == "" {
return []byte{}, nil 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 { if rel == nil {
return return
} }
// TODO: Switch to text/template like everything else.
if flagDebug { if flagDebug {
fmt.Printf("NAME: %s\n", rel.Name) fmt.Fprintf(i.out, "NAME: %s\n", rel.Name)
fmt.Printf("INFO: %s %s\n", timeconv.String(rel.Info.LastDeployed), rel.Info.Status) fmt.Fprintf(i.out, "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.Fprintf(i.out, "CHART: %s %s\n", rel.Chart.Metadata.Name, rel.Chart.Metadata.Version)
fmt.Printf("MANIFEST: %s\n", rel.Manifest) fmt.Fprintf(i.out, "MANIFEST: %s\n", rel.Manifest)
} else { } 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 { ...@@ -80,7 +80,7 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
list.filter = strings.Join(args, " ") list.filter = strings.Join(args, " ")
} }
if list.client == nil { if list.client == nil {
list.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) list.client = helm.NewClient(helm.Host(tillerHost))
} }
return list.run() return list.run()
}, },
......
...@@ -52,7 +52,7 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { ...@@ -52,7 +52,7 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command {
} }
status.release = args[0] status.release = args[0]
if status.client == nil { if status.client == nil {
status.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) status.client = helm.NewClient(helm.Host(tillerHost))
} }
return status.run() 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,9 +292,11 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install ...@@ -292,9 +292,11 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install
} }
// pre-install hooks // pre-install hooks
if !req.DisableHooks {
if err := s.execHook(r.Hooks, r.Name, preInstall); err != nil { if err := s.execHook(r.Hooks, r.Name, preInstall); err != nil {
return res, err return res, err
} }
}
// regular manifests // regular manifests
kubeCli := s.env.KubeClient kubeCli := s.env.KubeClient
...@@ -309,9 +311,11 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install ...@@ -309,9 +311,11 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install
} }
// post-install hooks // post-install hooks
if !req.DisableHooks {
if err := s.execHook(r.Hooks, r.Name, postInstall); err != nil { if err := s.execHook(r.Hooks, r.Name, postInstall); err != nil {
return res, err return res, err
} }
}
// This is a tricky case. The release has been created, but the result // This is a tricky case. The release has been created, but the result
// cannot be recorded. The truest thing to tell the user is that the // cannot be recorded. The truest thing to tell the user is that the
...@@ -382,9 +386,11 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR ...@@ -382,9 +386,11 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
rel.Info.Deleted = timeconv.Now() rel.Info.Deleted = timeconv.Now()
res := &services.UninstallReleaseResponse{Release: rel} res := &services.UninstallReleaseResponse{Release: rel}
if !req.DisableHooks {
if err := s.execHook(rel.Hooks, rel.Name, preDelete); err != nil { if err := s.execHook(rel.Hooks, rel.Name, preDelete); err != nil {
return res, err return res, err
} }
}
b := bytes.NewBuffer([]byte(rel.Manifest)) b := bytes.NewBuffer([]byte(rel.Manifest))
if err := s.env.KubeClient.Delete(s.env.Namespace, b); err != nil { if err := s.env.KubeClient.Delete(s.env.Namespace, b); err != nil {
...@@ -392,9 +398,11 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR ...@@ -392,9 +398,11 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
return nil, err return nil, err
} }
if !req.DisableHooks {
if err := s.execHook(rel.Hooks, rel.Name, postDelete); err != nil { if err := s.execHook(rel.Hooks, rel.Name, postDelete); err != nil {
return res, err return res, err
} }
}
if err := s.env.Releases.Update(rel); err != nil { if err := s.env.Releases.Update(rel); err != nil {
log.Printf("uninstall: Failed to store updated release: %s", err) log.Printf("uninstall: Failed to store updated release: %s", err)
......
...@@ -31,7 +31,6 @@ import ( ...@@ -31,7 +31,6 @@ import (
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
"k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/proto/hapi/services"
"k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/timeconv"
) )
var manifestWithHook = `apiVersion: v1 var manifestWithHook = `apiVersion: v1
...@@ -50,7 +49,27 @@ func rsFixture() *releaseServer { ...@@ -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} date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0}
return &release.Release{ return &release.Release{
Name: "angry-panda", Name: "angry-panda",
...@@ -59,15 +78,7 @@ func releaseMock() *release.Release { ...@@ -59,15 +78,7 @@ func releaseMock() *release.Release {
LastDeployed: &date, LastDeployed: &date,
Status: &release.Status{Code: release.Status_DEPLOYED}, Status: &release.Status{Code: release.Status_DEPLOYED},
}, },
Chart: &chart.Chart{ Chart: chartStub(),
Metadata: &chart.Metadata{
Name: "foo",
Version: "0.1.0-beta.1",
},
Templates: []*chart.Template{
{Name: "foo.tpl", Data: []byte("Hello")},
},
},
Config: &chart.Config{Raw: `name = "value"`}, Config: &chart.Config{Raw: `name = "value"`},
Hooks: []*release.Hook{ Hooks: []*release.Hook{
{ {
...@@ -88,14 +99,9 @@ func TestInstallRelease(t *testing.T) { ...@@ -88,14 +99,9 @@ func TestInstallRelease(t *testing.T) {
c := context.Background() c := context.Background()
rs := rsFixture() rs := rsFixture()
// TODO: Refactor this into a mock.
req := &services.InstallReleaseRequest{ req := &services.InstallReleaseRequest{
Chart: &chart.Chart{ Chart: chartStub(),
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")},
{Name: "hooks", Data: []byte(manifestWithHook)},
},
},
} }
res, err := rs.InstallRelease(c, req) res, err := rs.InstallRelease(c, req)
if err != nil { if err != nil {
...@@ -144,17 +150,7 @@ func TestInstallReleaseDryRun(t *testing.T) { ...@@ -144,17 +150,7 @@ func TestInstallReleaseDryRun(t *testing.T) {
rs := rsFixture() rs := rsFixture()
req := &services.InstallReleaseRequest{ req := &services.InstallReleaseRequest{
Chart: &chart.Chart{ Chart: chartStub(),
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)},
},
},
DryRun: true, DryRun: true,
} }
res, err := rs.InstallRelease(c, req) res, err := rs.InstallRelease(c, req)
...@@ -198,30 +194,29 @@ func TestInstallReleaseDryRun(t *testing.T) { ...@@ -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) { func TestUninstallRelease(t *testing.T) {
c := context.Background() c := context.Background()
rs := rsFixture() rs := rsFixture()
rs.env.Releases.Create(&release.Release{ rs.env.Releases.Create(releaseStub())
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,
},
},
},
})
req := &services.UninstallReleaseRequest{ req := &services.UninstallReleaseRequest{
Name: "angry-panda", Name: "angry-panda",
...@@ -249,10 +244,31 @@ func TestUninstallRelease(t *testing.T) { ...@@ -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) { func TestGetReleaseContent(t *testing.T) {
c := context.Background() c := context.Background()
rs := rsFixture() rs := rsFixture()
rel := releaseMock() rel := releaseStub()
if err := rs.env.Releases.Create(rel); err != nil { if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err) t.Fatalf("Could not store mock release: %s", err)
} }
...@@ -270,7 +286,7 @@ func TestGetReleaseContent(t *testing.T) { ...@@ -270,7 +286,7 @@ func TestGetReleaseContent(t *testing.T) {
func TestGetReleaseStatus(t *testing.T) { func TestGetReleaseStatus(t *testing.T) {
c := context.Background() c := context.Background()
rs := rsFixture() rs := rsFixture()
rel := releaseMock() rel := releaseStub()
if err := rs.env.Releases.Create(rel); err != nil { if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err) t.Fatalf("Could not store mock release: %s", err)
} }
...@@ -289,7 +305,7 @@ func TestListReleases(t *testing.T) { ...@@ -289,7 +305,7 @@ func TestListReleases(t *testing.T) {
rs := rsFixture() rs := rsFixture()
num := 7 num := 7
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
rel := releaseMock() rel := releaseStub()
rel.Name = fmt.Sprintf("rel-%d", i) rel.Name = fmt.Sprintf("rel-%d", i)
if err := rs.env.Releases.Create(rel); err != nil { if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err) t.Fatalf("Could not store mock release: %s", err)
...@@ -313,7 +329,7 @@ func TestListReleasesSort(t *testing.T) { ...@@ -313,7 +329,7 @@ func TestListReleasesSort(t *testing.T) {
// sort. // sort.
num := 7 num := 7
for i := num; i > 0; i-- { for i := num; i > 0; i-- {
rel := releaseMock() rel := releaseStub()
rel.Name = fmt.Sprintf("rel-%d", i) rel.Name = fmt.Sprintf("rel-%d", i)
if err := rs.env.Releases.Create(rel); err != nil { if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err) t.Fatalf("Could not store mock release: %s", err)
...@@ -356,7 +372,7 @@ func TestListReleasesFilter(t *testing.T) { ...@@ -356,7 +372,7 @@ func TestListReleasesFilter(t *testing.T) {
} }
num := 7 num := 7
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
rel := releaseMock() rel := releaseStub()
rel.Name = names[i] rel.Name = names[i]
if err := rs.env.Releases.Create(rel); err != nil { if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err) 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 { ...@@ -38,19 +38,14 @@ type options struct {
chart string chart string
// if set dry-run helm client calls // if set dry-run helm client calls
dryRun bool dryRun bool
// if set, skip running hooks
disableHooks bool
// release list options are applied directly to the list releases request // release list options are applied directly to the list releases request
listReq rls.ListReleasesRequest listReq rls.ListReleasesRequest
// release install options are applied directly to the install release request // release install options are applied directly to the install release request
instReq rls.InstallReleaseRequest 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"). // Home specifies the location of helm home, (default = "$HOME/.helm").
func Home(home string) Option { func Home(home string) Option {
return func(opts *options) { return func(opts *options) {
...@@ -124,6 +119,34 @@ func ReleaseName(name string) InstallOption { ...@@ -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 // ContentOption -- TODO
type ContentOption func(*options) type ContentOption func(*options)
...@@ -163,12 +186,16 @@ func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient ...@@ -163,12 +186,16 @@ func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient
} }
o.instReq.Chart = chr o.instReq.Chart = chr
o.instReq.DryRun = o.dryRun o.instReq.DryRun = o.dryRun
o.instReq.DisableHooks = o.disableHooks
return rlc.InstallRelease(context.TODO(), &o.instReq) return rlc.InstallRelease(context.TODO(), &o.instReq)
} }
// Executes tiller.UninstallRelease RPC. // Executes tiller.UninstallRelease RPC.
func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) { func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) {
for _, opt := range opts {
opt(o)
}
if o.dryRun { if o.dryRun {
// In the dry run case, just see if the release exists // In the dry run case, just see if the release exists
r, err := o.rpcGetReleaseContent(rlsName, rlc) r, err := o.rpcGetReleaseContent(rlsName, rlc)
...@@ -177,8 +204,7 @@ func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, ...@@ -177,8 +204,7 @@ func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient,
} }
return &rls.UninstallReleaseResponse{Release: r.Release}, nil return &rls.UninstallReleaseResponse{Release: r.Release}, nil
} }
return rlc.UninstallRelease(context.TODO(), &rls.UninstallReleaseRequest{Name: rlsName, DisableHooks: o.disableHooks})
return rlc.UninstallRelease(context.TODO(), &rls.UninstallReleaseRequest{Name: rlsName})
} }
// Executes tiller.UpdateRelease RPC. // Executes tiller.UpdateRelease RPC.
......
...@@ -248,6 +248,8 @@ type InstallReleaseRequest struct { ...@@ -248,6 +248,8 @@ type InstallReleaseRequest struct {
// namespace, otherwise the server will return an error. If it is not // namespace, otherwise the server will return an error. If it is not
// supplied, the server will autogenerate one. // supplied, the server will autogenerate one.
Name string `protobuf:"bytes,4,opt,name=name" json:"name,omitempty"` 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{} } func (m *InstallReleaseRequest) Reset() { *m = InstallReleaseRequest{} }
...@@ -290,6 +292,8 @@ func (m *InstallReleaseResponse) GetRelease() *hapi_release3.Release { ...@@ -290,6 +292,8 @@ func (m *InstallReleaseResponse) GetRelease() *hapi_release3.Release {
type UninstallReleaseRequest struct { type UninstallReleaseRequest struct {
// Name is the name of the release to delete. // Name is the name of the release to delete.
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` 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{} } func (m *UninstallReleaseRequest) Reset() { *m = UninstallReleaseRequest{} }
...@@ -586,48 +590,50 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ ...@@ -586,48 +590,50 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{
} }
var fileDescriptor0 = []byte{ var fileDescriptor0 = []byte{
// 688 bytes of a gzipped FileDescriptorProto // 720 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x55, 0xdd, 0x4e, 0x13, 0x41, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x55, 0xdd, 0x6e, 0xd3, 0x4a,
0x14, 0x66, 0x69, 0x69, 0xcb, 0x41, 0x48, 0x39, 0x96, 0xb6, 0xee, 0x85, 0x31, 0x9b, 0xa8, 0x88, 0x10, 0xae, 0x9b, 0x34, 0x49, 0xa7, 0x3f, 0x4a, 0xf7, 0xa4, 0x49, 0x8e, 0x2f, 0x8e, 0x8e, 0x8c,
0xb2, 0xd5, 0x7a, 0x6f, 0x52, 0xa0, 0x21, 0x84, 0x5a, 0x92, 0xa9, 0x68, 0xe2, 0x85, 0x64, 0x81, 0x80, 0x52, 0xa8, 0x03, 0xe1, 0x1e, 0x29, 0x6d, 0xa3, 0x52, 0x35, 0xa4, 0xd2, 0x86, 0x82, 0xc4,
0xa9, 0xac, 0x59, 0x76, 0xeb, 0xce, 0x94, 0xc8, 0x23, 0xf8, 0x08, 0xbe, 0x89, 0x0f, 0xe4, 0x83, 0x05, 0x91, 0xdb, 0x6c, 0xa8, 0xc1, 0xf5, 0x06, 0xef, 0xa6, 0xa2, 0x8f, 0xc0, 0x1b, 0x71, 0xc5,
0x38, 0x3f, 0x3b, 0x9b, 0x6e, 0xd9, 0xd5, 0x86, 0x9b, 0xdd, 0x99, 0xfd, 0xbe, 0x39, 0xdf, 0x99, 0xd3, 0xf0, 0x20, 0xec, 0x8f, 0xd7, 0x8a, 0x13, 0x1b, 0xa2, 0xde, 0x38, 0xbb, 0x3b, 0xdf, 0xce,
0xef, 0x9c, 0xd3, 0x82, 0x7d, 0xe5, 0x4d, 0xfc, 0x0e, 0xa3, 0xf1, 0x8d, 0x7f, 0x41, 0x59, 0x87, 0x37, 0xfe, 0x66, 0x3e, 0x07, 0xec, 0x6b, 0x6f, 0xe2, 0xb7, 0x18, 0x89, 0x6e, 0xfd, 0x2b, 0xc2,
0xfb, 0x41, 0x40, 0x63, 0x77, 0x12, 0x47, 0x3c, 0xc2, 0x86, 0xc4, 0x5c, 0x83, 0xb9, 0x1a, 0xb3, 0x5a, 0xdc, 0x0f, 0x02, 0x12, 0xb9, 0x93, 0x88, 0x72, 0x8a, 0x6a, 0x32, 0xe6, 0x9a, 0x98, 0xab,
0x9b, 0xea, 0xc4, 0xc5, 0x95, 0x17, 0x73, 0xfd, 0xd4, 0x6c, 0xbb, 0x35, 0xfb, 0x3d, 0x0a, 0xc7, 0x63, 0x76, 0x5d, 0xdd, 0xb8, 0xba, 0xf6, 0x22, 0xae, 0x9f, 0x1a, 0x6d, 0x37, 0x66, 0xcf, 0x69,
0xfe, 0xd7, 0x04, 0xd0, 0x12, 0x31, 0x0d, 0xa8, 0xc7, 0xa8, 0x79, 0x67, 0x0e, 0x19, 0xcc, 0x0f, 0x38, 0xf6, 0x3f, 0xc5, 0x01, 0x4d, 0x11, 0x91, 0x80, 0x78, 0x8c, 0x98, 0xdf, 0xd4, 0x25, 0x13,
0xc7, 0x91, 0x06, 0x9c, 0x3f, 0x16, 0x3c, 0x1c, 0xf8, 0x8c, 0x13, 0x0d, 0x31, 0x42, 0xbf, 0x4f, 0xf3, 0xc3, 0x31, 0xd5, 0x01, 0xe7, 0x97, 0x05, 0xff, 0xf4, 0x7c, 0xc6, 0xb1, 0x0e, 0x31, 0x4c,
0x29, 0xe3, 0xd8, 0x80, 0x95, 0xc0, 0xbf, 0xf6, 0x79, 0xdb, 0x7a, 0x62, 0x6d, 0x97, 0x88, 0xde, 0xbe, 0x4e, 0x09, 0xe3, 0xa8, 0x06, 0x6b, 0x81, 0x7f, 0xe3, 0xf3, 0xa6, 0xf5, 0xbf, 0xb5, 0x57,
0x60, 0x13, 0x2a, 0xd1, 0x78, 0xcc, 0x28, 0x6f, 0x2f, 0x8b, 0xcf, 0xab, 0x24, 0xd9, 0xe1, 0x3b, 0xc0, 0x7a, 0x83, 0xea, 0x50, 0xa2, 0xe3, 0x31, 0x23, 0xbc, 0xb9, 0x2a, 0x8e, 0xd7, 0x71, 0xbc,
0xa8, 0xb2, 0x28, 0xe6, 0x67, 0xe7, 0xb7, 0xed, 0x92, 0x00, 0x36, 0xba, 0x4f, 0xdd, 0xbc, 0x3b, 0x43, 0xaf, 0xa0, 0xcc, 0x68, 0xc4, 0x87, 0x97, 0x77, 0xcd, 0x82, 0x08, 0x6c, 0xb7, 0x1f, 0xba,
0xb9, 0x52, 0x69, 0x24, 0x88, 0xae, 0x7c, 0xec, 0xdd, 0x92, 0x0a, 0x53, 0x6f, 0x19, 0x77, 0xec, 0x59, 0xef, 0xe4, 0x4a, 0xa6, 0x81, 0x00, 0xba, 0xf2, 0x71, 0x78, 0x87, 0x4b, 0x4c, 0xfd, 0xca,
0x07, 0x9c, 0xc6, 0xed, 0xb2, 0x8e, 0xab, 0x77, 0x78, 0x08, 0xa0, 0xe2, 0x46, 0xf1, 0xa5, 0xc0, 0xbc, 0x63, 0x3f, 0xe0, 0x24, 0x6a, 0x16, 0x75, 0x5e, 0xbd, 0x43, 0x27, 0x00, 0x2a, 0x2f, 0x8d,
0x56, 0x54, 0xe8, 0xed, 0x05, 0x42, 0x9f, 0x48, 0x3e, 0x59, 0x65, 0x66, 0xe9, 0x7c, 0x81, 0x9a, 0x46, 0x22, 0xb6, 0xa6, 0x52, 0xef, 0x2d, 0x91, 0xfa, 0x5c, 0xe2, 0xf1, 0x3a, 0x33, 0x4b, 0xe7,
0x21, 0x38, 0x5d, 0xa8, 0x68, 0x79, 0x5c, 0x83, 0xea, 0xe9, 0xf0, 0x78, 0x78, 0xf2, 0x69, 0x58, 0x23, 0x54, 0x0c, 0xc0, 0x69, 0x43, 0x49, 0xd3, 0xa3, 0x0d, 0x28, 0x5f, 0xf4, 0xcf, 0xfa, 0xe7,
0x5f, 0xc2, 0x1a, 0x94, 0x87, 0xbd, 0xf7, 0xfd, 0xba, 0x85, 0x9b, 0xb0, 0x3e, 0xe8, 0x8d, 0x3e, 0xef, 0xfb, 0xd5, 0x15, 0x54, 0x81, 0x62, 0xbf, 0xf3, 0xa6, 0x5b, 0xb5, 0xd0, 0x0e, 0x6c, 0xf5,
0x9c, 0x91, 0xfe, 0xa0, 0xdf, 0x1b, 0xf5, 0x0f, 0xea, 0xcb, 0xce, 0x63, 0x58, 0x4d, 0xe3, 0x62, 0x3a, 0x83, 0xb7, 0x43, 0xdc, 0xed, 0x75, 0x3b, 0x83, 0xee, 0x71, 0x75, 0xd5, 0xf9, 0x0f, 0xd6,
0x15, 0x4a, 0xbd, 0xd1, 0xbe, 0x3e, 0x72, 0xd0, 0x17, 0x2b, 0xcb, 0xf9, 0x69, 0x41, 0x23, 0x6b, 0x93, 0xbc, 0xa8, 0x0c, 0x85, 0xce, 0xe0, 0x48, 0x5f, 0x39, 0xee, 0x8a, 0x95, 0xe5, 0x7c, 0xb7,
0x23, 0x9b, 0x44, 0x21, 0xa3, 0xd2, 0xc7, 0x8b, 0x68, 0x1a, 0xa6, 0x3e, 0xaa, 0x0d, 0x22, 0x94, 0xa0, 0x96, 0x96, 0x91, 0x4d, 0x68, 0xc8, 0x88, 0xd4, 0xf1, 0x8a, 0x4e, 0xc3, 0x44, 0x47, 0xb5,
0x43, 0xfa, 0xc3, 0xb8, 0xa8, 0xd6, 0x92, 0xc9, 0x23, 0xee, 0x05, 0xca, 0x41, 0xc1, 0x54, 0x1b, 0x41, 0x08, 0x8a, 0x21, 0xf9, 0x66, 0x54, 0x54, 0x6b, 0x89, 0xe4, 0x94, 0x7b, 0x81, 0x52, 0x50,
0x7c, 0x03, 0xb5, 0xa4, 0x6a, 0x4c, 0x78, 0x53, 0xda, 0x5e, 0xeb, 0x6e, 0xe9, 0xfb, 0x9b, 0xfa, 0x20, 0xd5, 0x06, 0xbd, 0x80, 0x4a, 0xdc, 0x35, 0x26, 0xb4, 0x29, 0xec, 0x6d, 0xb4, 0x77, 0xf5,
0x26, 0x8a, 0x24, 0xa5, 0x39, 0xbb, 0xd0, 0x3a, 0xa4, 0x26, 0x93, 0x11, 0xf7, 0xf8, 0x34, 0xad, 0xfb, 0x9b, 0xfe, 0xc6, 0x8c, 0x38, 0x81, 0x39, 0x07, 0xd0, 0x38, 0x21, 0xa6, 0x92, 0x01, 0xf7,
0xaa, 0xd4, 0xf5, 0xae, 0xa9, 0x4a, 0x46, 0xea, 0x8a, 0xb5, 0xf3, 0x11, 0xda, 0x77, 0xe9, 0x49, 0xf8, 0x34, 0xe9, 0xaa, 0xe4, 0xf5, 0x6e, 0x88, 0x2a, 0x46, 0xf2, 0x8a, 0xb5, 0xf3, 0x0e, 0x9a,
0xf6, 0x39, 0x7c, 0x7c, 0x06, 0x65, 0xd9, 0x3f, 0x2a, 0xf7, 0xb5, 0x2e, 0x66, 0xb3, 0x39, 0x12, 0x8b, 0xf0, 0xb8, 0xfa, 0x0c, 0x3c, 0x7a, 0x04, 0x45, 0x39, 0x3f, 0xaa, 0xf6, 0x8d, 0x36, 0x4a,
0x08, 0x51, 0xb8, 0xe3, 0xce, 0xc6, 0xdd, 0x8f, 0x42, 0x4e, 0x43, 0xfe, 0xaf, 0x3c, 0x06, 0xf0, 0x57, 0x73, 0x2a, 0x22, 0x58, 0xc5, 0x1d, 0x77, 0x36, 0xef, 0x11, 0x0d, 0x39, 0x09, 0xf9, 0x9f,
0x28, 0x87, 0x9f, 0x24, 0xd2, 0x81, 0x6a, 0x22, 0xa1, 0xce, 0x14, 0xba, 0x60, 0x58, 0x4e, 0x13, 0xea, 0xe8, 0xc1, 0xbf, 0x19, 0xf8, 0xb8, 0x90, 0x16, 0x94, 0x63, 0x0a, 0x75, 0x27, 0x57, 0x05,
0x1a, 0xa7, 0x93, 0x4b, 0x8f, 0x53, 0x83, 0x68, 0x65, 0xa7, 0x05, 0x5b, 0x73, 0xdf, 0xb5, 0x82, 0x83, 0x72, 0xea, 0x50, 0xbb, 0x98, 0x8c, 0x3c, 0x4e, 0x4c, 0x44, 0x33, 0x3b, 0x0d, 0xd8, 0x9d,
0xf3, 0xcb, 0x82, 0xad, 0xa3, 0x90, 0x09, 0xcf, 0x83, 0xec, 0x11, 0x7c, 0x2e, 0x4a, 0x28, 0xa7, 0x3b, 0xd7, 0x0c, 0xce, 0x4f, 0x0b, 0x76, 0x4f, 0x43, 0x26, 0x34, 0x0f, 0xd2, 0x57, 0xd0, 0x63,
0x2d, 0x51, 0xde, 0xd4, 0xca, 0x7a, 0x24, 0xf7, 0xe5, 0x93, 0x68, 0x1c, 0x77, 0xa0, 0x72, 0xe3, 0xd1, 0x42, 0xe9, 0xb6, 0x98, 0x79, 0x47, 0x33, 0x6b, 0x4b, 0x1e, 0xc9, 0x27, 0xd6, 0x71, 0xb4,
0x05, 0xe2, 0x4c, 0xd6, 0x9b, 0x84, 0xa9, 0x46, 0x95, 0x24, 0x0c, 0x6c, 0x41, 0xf5, 0x32, 0xbe, 0x0f, 0xa5, 0x5b, 0x2f, 0x10, 0x77, 0xd2, 0xda, 0xc4, 0x48, 0x65, 0x55, 0x1c, 0x23, 0x50, 0x03,
0x3d, 0x8b, 0xa7, 0xa1, 0xaa, 0x77, 0x8d, 0x54, 0xc4, 0x96, 0x4c, 0xc3, 0xd4, 0x9a, 0xf2, 0x8c, 0xca, 0xa3, 0xe8, 0x6e, 0x18, 0x4d, 0x43, 0xd5, 0xef, 0x0a, 0x2e, 0x89, 0x2d, 0x9e, 0x86, 0x89,
0x35, 0x47, 0xd0, 0x9c, 0x4f, 0xed, 0xbe, 0xbe, 0x88, 0xe6, 0x38, 0x0d, 0xfd, 0xdc, 0x7b, 0xe6, 0x34, 0xc5, 0x19, 0xc9, 0x1f, 0xc0, 0xd6, 0xc8, 0x67, 0xde, 0x65, 0x40, 0x86, 0xd7, 0x94, 0x7e,
0x15, 0xe5, 0x18, 0xda, 0x77, 0xe9, 0xf7, 0xd4, 0xee, 0xfe, 0x5e, 0x81, 0x0d, 0xd3, 0x67, 0x7a, 0x61, 0xca, 0x09, 0x15, 0xbc, 0x19, 0x1f, 0xbe, 0x96, 0x67, 0xce, 0x29, 0xd4, 0xe7, 0xeb, 0xbf,
0x7a, 0xd1, 0x87, 0x07, 0xb3, 0x63, 0x83, 0x2f, 0x8a, 0x87, 0x7b, 0xee, 0x17, 0xca, 0xde, 0x59, 0xaf, 0x78, 0x18, 0x1a, 0x17, 0xa1, 0x9f, 0x29, 0x46, 0xd6, 0x44, 0x2c, 0x94, 0xb7, 0x9a, 0x51,
0x84, 0x9a, 0x14, 0x77, 0xe9, 0xb5, 0x85, 0x0c, 0xea, 0xf3, 0x7d, 0x8e, 0xbb, 0xf9, 0x31, 0x0a, 0xde, 0x19, 0x34, 0x17, 0x73, 0xde, 0xb3, 0xc0, 0xf6, 0x8f, 0x35, 0xd8, 0x36, 0x13, 0xab, 0xbf,
0xc6, 0xc7, 0x76, 0x17, 0xa5, 0x1b, 0x59, 0xbc, 0x81, 0xcd, 0x3b, 0x4d, 0x8d, 0xff, 0x0d, 0x93, 0x03, 0xc8, 0x87, 0xcd, 0x59, 0x03, 0xa2, 0x27, 0xf9, 0x9f, 0x89, 0xb9, 0x6f, 0x9d, 0xbd, 0xbf,
0x9d, 0x16, 0xbb, 0xb3, 0x30, 0x3f, 0xd5, 0xfd, 0x06, 0xeb, 0x99, 0x36, 0xc7, 0x02, 0xb7, 0xf2, 0x0c, 0x34, 0x1e, 0x93, 0x95, 0xe7, 0x16, 0x62, 0x50, 0x9d, 0x77, 0x0c, 0x3a, 0xc8, 0xce, 0x91,
0x66, 0xc4, 0x7e, 0xb9, 0x10, 0x37, 0xd5, 0xba, 0x86, 0x8d, 0x6c, 0x77, 0x62, 0x41, 0x80, 0xdc, 0x63, 0x44, 0xdb, 0x5d, 0x16, 0x6e, 0x68, 0xd1, 0x2d, 0xec, 0x2c, 0xd8, 0x03, 0xfd, 0x35, 0x4d,
0xf1, 0xb2, 0x5f, 0x2d, 0x46, 0x4e, 0xe5, 0x44, 0x1d, 0xe7, 0x5b, 0xb2, 0xa8, 0x8e, 0x05, 0x9d, 0xda, 0x77, 0x76, 0x6b, 0x69, 0x7c, 0xc2, 0xfb, 0x19, 0xb6, 0x52, 0x86, 0x41, 0x39, 0x6a, 0x65,
0x5e, 0x54, 0xc7, 0xa2, 0x4e, 0x77, 0x96, 0xf6, 0xe0, 0x73, 0xcd, 0xb0, 0xcf, 0x2b, 0xea, 0x9f, 0xb9, 0xcd, 0x7e, 0xba, 0x14, 0x36, 0xe1, 0xba, 0x81, 0xed, 0xf4, 0x08, 0xa3, 0x9c, 0x04, 0x99,
0xf3, 0xed, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x39, 0x3c, 0xcd, 0x3c, 0xd3, 0x07, 0x00, 0x00, 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