Add validate to Install / Upgrade commands that use kubernets to validate the manifest before reifying

pull/1217/merge^2
Ville Aikas 9 years ago
parent dbb84a1b9e
commit 12f6fe6900

@ -181,6 +181,10 @@ message UpdateReleaseRequest {
// DisableHooks causes the server to skip running any hooks for the upgrade. // DisableHooks causes the server to skip running any hooks for the upgrade.
bool disable_hooks = 5; bool disable_hooks = 5;
// Validate requests that Tiller uses kubernetes validation logic on the
// manifest before handing it off for reification
bool validate = 6;
} }
// UpdateReleaseResponse is the response to an update request. // UpdateReleaseResponse is the response to an update request.
@ -212,6 +216,10 @@ message InstallReleaseRequest {
// ReuseName requests that Tiller re-uses a name, instead of erroring out. // ReuseName requests that Tiller re-uses a name, instead of erroring out.
bool reuse_name = 7; bool reuse_name = 7;
// Validate requests that Tiller uses kubernetes validation logic on the
// manifest before handing it off for reification
bool validate = 8;
} }
// InstallReleaseResponse is the response from a release installation. // InstallReleaseResponse is the response from a release installation.

@ -69,6 +69,7 @@ type installCmd struct {
disableHooks bool disableHooks bool
replace bool replace bool
verify bool verify bool
validate bool
keyring string keyring string
out io.Writer out io.Writer
client helm.Interface client helm.Interface
@ -113,6 +114,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
f.Var(inst.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2") f.Var(inst.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2")
f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release") f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release")
f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it") f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it")
f.BoolVar(&inst.validate, "validate", false, "validate manifest before reifying it")
f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification") f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification")
return cmd return cmd
} }
@ -144,6 +146,7 @@ func (i *installCmd) run() error {
helm.ReleaseName(i.name), helm.ReleaseName(i.name),
helm.InstallDryRun(i.dryRun), helm.InstallDryRun(i.dryRun),
helm.InstallReuseName(i.replace), helm.InstallReuseName(i.replace),
helm.InstallValidate(i.validate),
helm.InstallDisableHooks(i.disableHooks)) helm.InstallDisableHooks(i.disableHooks))
if err != nil { if err != nil {
return prettyError(err) return prettyError(err)

@ -47,6 +47,7 @@ type upgradeCmd struct {
valuesFile string valuesFile string
values *values values *values
verify bool verify bool
validate bool
keyring string keyring string
} }
@ -82,6 +83,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
f.Var(upgrade.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2") f.Var(upgrade.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2")
f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks") f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks")
f.BoolVar(&upgrade.verify, "verify", false, "verify the provenance of the chart before upgrading") f.BoolVar(&upgrade.verify, "verify", false, "verify the provenance of the chart before upgrading")
f.BoolVar(&upgrade.validate, "validate", false, "validate manifest before reifying it")
f.StringVar(&upgrade.keyring, "keyring", defaultKeyring(), "the path to the keyring that contains public singing keys") f.StringVar(&upgrade.keyring, "keyring", defaultKeyring(), "the path to the keyring that contains public singing keys")
return cmd return cmd
@ -123,7 +125,7 @@ func (u *upgradeCmd) run() error {
return err return err
} }
_, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun), helm.UpgradeDisableHooks(u.disableHooks)) _, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun), helm.UpgradeDisableHooks(u.disableHooks), helm.UpgradeValidate(u.validate))
if err != nil { if err != nil {
return fmt.Errorf("UPGRADE FAILED: %v", prettyError(err)) return fmt.Errorf("UPGRADE FAILED: %v", prettyError(err))
} }

@ -21,6 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"os"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
@ -38,6 +39,8 @@ import (
"k8s.io/helm/pkg/timeconv" "k8s.io/helm/pkg/timeconv"
"k8s.io/helm/pkg/version" "k8s.io/helm/pkg/version"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource"
) )
var srv *releaseServer var srv *releaseServer
@ -273,6 +276,16 @@ func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.R
kubeCli := s.env.KubeClient kubeCli := s.env.KubeClient
original := bytes.NewBufferString(originalRelease.Manifest) original := bytes.NewBufferString(originalRelease.Manifest)
modified := bytes.NewBufferString(updatedRelease.Manifest) modified := bytes.NewBufferString(updatedRelease.Manifest)
// Validate the manifest
if req.Validate {
log.Printf("Validating manifest: %s\n", modified)
err := validateResources(updatedRelease.Namespace, modified)
if err != nil {
return nil, err
}
}
if err := kubeCli.Update(updatedRelease.Namespace, original, modified); err != nil { if err := kubeCli.Update(updatedRelease.Namespace, original, modified); err != nil {
return nil, err return nil, err
} }
@ -563,6 +576,16 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install
// regular manifests // regular manifests
kubeCli := s.env.KubeClient kubeCli := s.env.KubeClient
b := bytes.NewBufferString(r.Manifest) b := bytes.NewBufferString(r.Manifest)
// Validate the manifest
if req.Validate {
log.Printf("Validating manifest: %s\n", b)
err := validateResources(r.Namespace, b)
if err != nil {
return res, err
}
}
if err := kubeCli.Create(r.Namespace, b); err != nil { if err := kubeCli.Create(r.Namespace, b); err != nil {
log.Printf("warning: Release %q failed: %s", r.Name, err) log.Printf("warning: Release %q failed: %s", r.Name, err)
r.Info.Status.Code = release.Status_FAILED r.Info.Status.Code = release.Status_FAILED
@ -691,6 +714,39 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
return res, nil return res, nil
} }
// validateResources takes a namespace and a manifest (fully expanded set of templates) and
// validates resources.
func validateResources(namespace string, manifest *bytes.Buffer) error {
f := cmdutil.NewFactory(nil)
schema, err := f.Validator(true, os.TempDir())
if err != nil {
return err
}
mapper, typer := f.Object(true)
r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)).
Schema(schema).
NamespaceParam(namespace).DefaultNamespace().
Stream(manifest, "release").
Flatten().
Do()
err = r.Err()
if err != nil {
return err
}
err = r.Visit(func(info *resource.Info, err error) error {
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
// byName implements the sort.Interface for []*release.Release. // byName implements the sort.Interface for []*release.Release.
type byName []*release.Release type byName []*release.Release

@ -17,6 +17,7 @@ limitations under the License.
package main package main
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -59,6 +60,22 @@ data:
name: value name: value
` `
var invalidResource = `apiVersion: v1
kind: ConfigMap
metadata:
naaame: test-cm
data:
name: value
`
var validResource = `apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
data:
name: value
`
func rsFixture() *releaseServer { func rsFixture() *releaseServer {
return &releaseServer{ return &releaseServer{
env: mockEnvironment(), env: mockEnvironment(),
@ -483,6 +500,148 @@ func TestInstallReleaseReuseName(t *testing.T) {
} }
} }
func TestInstallReleaseWithValidationFailsForInvalid(t *testing.T) {
c := context.Background()
rs := rsFixture()
// TODO: Refactor this into a mock.
req := &services.InstallReleaseRequest{
Namespace: "spaced",
Validate: true,
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "invalid", Data: []byte(invalidResource)},
},
},
}
_, err := rs.InstallRelease(c, req)
if err == nil {
t.Fatalf("Install succeeded but was expected to fail")
}
if !strings.Contains(err.Error(), "naaame") {
t.Errorf("didn't fail with expected error")
}
}
func TestInstallReleaseWithValidationWorksForValid(t *testing.T) {
c := context.Background()
rs := rsFixture()
// TODO: Refactor this into a mock.
req := &services.InstallReleaseRequest{
Namespace: "spaced",
Validate: true,
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "invalid", Data: []byte(validResource)},
},
},
}
res, err := rs.InstallRelease(c, req)
if err != nil {
t.Fatalf("Failed install: %s %s", err, req.Chart)
}
if res.Release.Name == "" {
t.Errorf("Expected release name.")
}
if res.Release.Namespace != "spaced" {
t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Release.Namespace)
}
rel, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version)
if err != nil {
t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases)
}
if len(rel.Manifest) == 0 {
t.Errorf("Expected manifest in %v", res)
}
}
func TestUpdateReleaseWithValidationFailsForInvalid(t *testing.T) {
c := context.Background()
rs := rsFixture()
req := &services.InstallReleaseRequest{
Name: "myspecialone",
Namespace: "spaced",
Validate: true,
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "invalid", Data: []byte(validResource)},
},
},
}
_, err := rs.InstallRelease(c, req)
if err != nil {
t.Fatalf("Install failed: %v", err)
}
uReq := &services.UpdateReleaseRequest{
Name: "myspecialone",
Validate: true,
DisableHooks: true,
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "invalid", Data: []byte(invalidResource)},
},
},
}
_, err = rs.UpdateRelease(c, uReq)
if err == nil {
t.Fatalf("Install succeeded but was expected to fail")
}
if !strings.Contains(err.Error(), "naaame") {
t.Errorf("didn't fail with expected error")
}
}
func TestUpdateReleaseWithValidationWorksForValid(t *testing.T) {
c := context.Background()
rs := rsFixture()
req := &services.InstallReleaseRequest{
Name: "myspecialone",
Namespace: "spaced",
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "invalid", Data: []byte(invalidResource)},
},
},
}
_, err := rs.InstallRelease(c, req)
if err != nil {
t.Fatalf("Install failed: %v", err)
}
uReq := &services.UpdateReleaseRequest{
Name: "myspecialone",
Validate: true,
DisableHooks: true,
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "invalid", Data: []byte(validResource)},
},
},
}
_, err = rs.UpdateRelease(c, uReq)
if err != nil {
t.Fatalf("Install failed: %v", err)
}
}
func TestUpdateRelease(t *testing.T) { func TestUpdateRelease(t *testing.T) {
c := context.Background() c := context.Background()
rs := rsFixture() rs := rsFixture()
@ -930,6 +1089,11 @@ func TestListReleasesFilter(t *testing.T) {
} }
} }
func testValidateResources(t *testing.T) {
b := bytes.NewBufferString("")
validateResources("default", b)
}
func mockEnvironment() *environment.Environment { func mockEnvironment() *environment.Environment {
e := environment.New() e := environment.New()
e.Releases = storage.Init(driver.NewMemory()) e.Releases = storage.Init(driver.NewMemory())

@ -38,6 +38,8 @@ 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 validate manifest
validate bool
// if set, re-use an existing name // if set, re-use an existing name
reuseName bool reuseName bool
// if set, skip running hooks // if set, skip running hooks
@ -181,6 +183,14 @@ func UpgradeDryRun(dry bool) UpdateOption {
} }
} }
// UpgradeValidate will (if true) instruct Tiller to validate manifest before
// reifying
func UpgradeValidate(validate bool) UpdateOption {
return func(opts *options) {
opts.validate = validate
}
}
// InstallDisableHooks disables hooks during installation. // InstallDisableHooks disables hooks during installation.
func InstallDisableHooks(disable bool) InstallOption { func InstallDisableHooks(disable bool) InstallOption {
return func(opts *options) { return func(opts *options) {
@ -195,6 +205,14 @@ func InstallDryRun(dry bool) InstallOption {
} }
} }
// InstallValidate will (if true) instruct Tiller to validate manifest before
// reifying
func InstallValidate(validate bool) InstallOption {
return func(opts *options) {
opts.validate = validate
}
}
// InstallReuseName will (if true) instruct Tiller to re-use an existing name. // InstallReuseName will (if true) instruct Tiller to re-use an existing name.
func InstallReuseName(reuse bool) InstallOption { func InstallReuseName(reuse bool) InstallOption {
return func(opts *options) { return func(opts *options) {
@ -268,6 +286,7 @@ func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient
o.instReq.DryRun = o.dryRun o.instReq.DryRun = o.dryRun
o.instReq.DisableHooks = o.disableHooks o.instReq.DisableHooks = o.disableHooks
o.instReq.ReuseName = o.reuseName o.instReq.ReuseName = o.reuseName
o.instReq.Validate = o.validate
return rlc.InstallRelease(context.TODO(), &o.instReq) return rlc.InstallRelease(context.TODO(), &o.instReq)
} }
@ -301,6 +320,7 @@ func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.Relea
o.updateReq.Chart = chr o.updateReq.Chart = chr
o.updateReq.DryRun = o.dryRun o.updateReq.DryRun = o.dryRun
o.updateReq.Name = rlsName o.updateReq.Name = rlsName
o.updateReq.Validate = o.validate
return rlc.UpdateRelease(context.TODO(), &o.updateReq) return rlc.UpdateRelease(context.TODO(), &o.updateReq)
} }

@ -240,6 +240,9 @@ type UpdateReleaseRequest struct {
DryRun bool `protobuf:"varint,4,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"` DryRun bool `protobuf:"varint,4,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"`
// DisableHooks causes the server to skip running any hooks for the upgrade. // DisableHooks causes the server to skip running any hooks for the upgrade.
DisableHooks bool `protobuf:"varint,5,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"` DisableHooks bool `protobuf:"varint,5,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"`
// Validate requests that Tiller uses kubernetes validation logic on the
// manifest before handing it off for reification
Validate bool `protobuf:"varint,6,opt,name=validate" json:"validate,omitempty"`
} }
func (m *UpdateReleaseRequest) Reset() { *m = UpdateReleaseRequest{} } func (m *UpdateReleaseRequest) Reset() { *m = UpdateReleaseRequest{} }
@ -298,6 +301,9 @@ type InstallReleaseRequest struct {
Namespace string `protobuf:"bytes,6,opt,name=namespace" json:"namespace,omitempty"` Namespace string `protobuf:"bytes,6,opt,name=namespace" json:"namespace,omitempty"`
// ReuseName requests that Tiller re-uses a name, instead of erroring out. // ReuseName requests that Tiller re-uses a name, instead of erroring out.
ReuseName bool `protobuf:"varint,7,opt,name=reuse_name,json=reuseName" json:"reuse_name,omitempty"` ReuseName bool `protobuf:"varint,7,opt,name=reuse_name,json=reuseName" json:"reuse_name,omitempty"`
// Validate requests that Tiller uses kubernetes validation logic on the
// manifest before handing it off for reification
Validate bool `protobuf:"varint,8,opt,name=validate" json:"validate,omitempty"`
} }
func (m *InstallReleaseRequest) Reset() { *m = InstallReleaseRequest{} } func (m *InstallReleaseRequest) Reset() { *m = InstallReleaseRequest{} }
@ -696,61 +702,62 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{
} }
var fileDescriptor0 = []byte{ var fileDescriptor0 = []byte{
// 893 bytes of a gzipped FileDescriptorProto // 909 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0x5f, 0x6f, 0xe3, 0x44, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x6e, 0xe3, 0x44,
0x10, 0x3f, 0x27, 0xa9, 0x93, 0x4c, 0xda, 0x2a, 0xdd, 0x6b, 0x1b, 0xd7, 0x02, 0x74, 0x32, 0x82, 0x14, 0xde, 0xfc, 0xd4, 0x71, 0x4e, 0x7f, 0xd4, 0xce, 0xf6, 0xc7, 0xb5, 0x00, 0xad, 0x8c, 0x60,
0x0b, 0x07, 0x24, 0x10, 0x5e, 0x11, 0x52, 0x2e, 0x17, 0xf5, 0xaa, 0x0b, 0x39, 0x69, 0x43, 0x41, 0xcb, 0x02, 0x29, 0x84, 0x5b, 0x84, 0xd4, 0xcd, 0x46, 0xdd, 0x6a, 0x43, 0x56, 0x9a, 0x50, 0x90,
0xe2, 0x81, 0xc8, 0x4d, 0x36, 0x17, 0x83, 0x6b, 0x07, 0xef, 0x26, 0xa2, 0x1f, 0x81, 0xcf, 0xc0, 0xb8, 0x20, 0x72, 0x93, 0xc9, 0xc6, 0xe0, 0xda, 0xc1, 0x33, 0x89, 0xe8, 0x23, 0xf0, 0x04, 0x5c,
0x1b, 0xdf, 0x83, 0x4f, 0xc6, 0x0b, 0xeb, 0xfd, 0xe3, 0xc6, 0xa9, 0x4d, 0x7d, 0x79, 0x89, 0xbd, 0x70, 0xc7, 0x5b, 0xf1, 0x36, 0xcc, 0xaf, 0x1b, 0xbb, 0x36, 0xf5, 0xe6, 0x26, 0xf6, 0xcc, 0xf9,
0x3b, 0xbf, 0xfd, 0xcd, 0xcc, 0x6f, 0x76, 0xc6, 0x01, 0x7b, 0xe9, 0xae, 0xbc, 0x2e, 0x25, 0xd1, 0xe6, 0x3b, 0xe7, 0x7c, 0x67, 0xce, 0x71, 0xc0, 0x9d, 0xfb, 0x8b, 0xe0, 0x9c, 0x92, 0x64, 0x15,
0xc6, 0x9b, 0x11, 0xda, 0x65, 0x9e, 0xef, 0x93, 0xa8, 0xb3, 0x8a, 0x42, 0x16, 0xa2, 0xd3, 0xd8, 0x4c, 0x08, 0x3d, 0x67, 0x41, 0x18, 0x92, 0xa4, 0xb3, 0x48, 0x62, 0x16, 0xa3, 0x43, 0x61, 0xeb,
0xd6, 0xd1, 0xb6, 0x8e, 0xb4, 0xd9, 0xe7, 0xe2, 0xc4, 0x6c, 0xe9, 0x46, 0x4c, 0xfe, 0x4a, 0xb4, 0x18, 0x5b, 0x47, 0xd9, 0xdc, 0x63, 0x79, 0x62, 0x32, 0xf7, 0x13, 0xa6, 0x7e, 0x15, 0xda, 0x3d,
0xdd, 0xda, 0xde, 0x0f, 0x83, 0x85, 0xf7, 0x4e, 0x19, 0xa4, 0x8b, 0x88, 0xf8, 0xc4, 0xa5, 0x44, 0x59, 0xdf, 0x8f, 0xa3, 0x59, 0xf0, 0x4e, 0x1b, 0x94, 0x8b, 0x84, 0x84, 0xc4, 0xa7, 0xc4, 0x3c,
0x3f, 0x53, 0x87, 0xb4, 0xcd, 0x0b, 0x16, 0xa1, 0x32, 0x5c, 0xa4, 0x0c, 0x94, 0xb9, 0x6c, 0x4d, 0x33, 0x87, 0x8c, 0x2d, 0x88, 0x66, 0xb1, 0x36, 0x9c, 0x66, 0x0c, 0x94, 0xf9, 0x6c, 0x49, 0x33,
0x53, 0x7c, 0x1b, 0x12, 0x51, 0x2f, 0x0c, 0xf4, 0x53, 0xda, 0x9c, 0xbf, 0x4b, 0xf0, 0x74, 0xe4, 0x7c, 0x2b, 0x92, 0xd0, 0x20, 0x8e, 0xcc, 0x53, 0xd9, 0xbc, 0x7f, 0xea, 0xf0, 0x74, 0x10, 0x50,
0x51, 0x86, 0xe5, 0x41, 0x8a, 0xc9, 0xef, 0x6b, 0x42, 0x19, 0x3a, 0x85, 0x03, 0xdf, 0xbb, 0xf5, 0x86, 0xd5, 0x41, 0x8a, 0xc9, 0xef, 0x4b, 0x42, 0x19, 0x3a, 0x84, 0xad, 0x30, 0xb8, 0x0d, 0x98,
0x98, 0x65, 0x3c, 0x33, 0xda, 0x65, 0x2c, 0x17, 0xe8, 0x1c, 0xcc, 0x70, 0xb1, 0xa0, 0x84, 0x59, 0x53, 0x7b, 0x56, 0x3b, 0x6b, 0x60, 0xb5, 0x40, 0xc7, 0x60, 0xc5, 0xb3, 0x19, 0x25, 0xcc, 0xa9,
0x25, 0xbe, 0x5d, 0xc7, 0x6a, 0x85, 0xbe, 0x83, 0x2a, 0x0d, 0x23, 0x36, 0xbd, 0xb9, 0xb3, 0xca, 0xf3, 0xed, 0x36, 0xd6, 0x2b, 0xf4, 0x1d, 0xb4, 0x68, 0x9c, 0xb0, 0xf1, 0xcd, 0x9d, 0xd3, 0xe0,
0xdc, 0x70, 0xdc, 0xfb, 0xa4, 0x93, 0x25, 0x45, 0x27, 0xf6, 0x34, 0xe1, 0xc0, 0x4e, 0xfc, 0xf3, 0x86, 0xbd, 0xee, 0x27, 0x9d, 0x22, 0x29, 0x3a, 0xc2, 0xd3, 0x88, 0x03, 0x3b, 0xe2, 0xe7, 0xe5,
0xf2, 0x0e, 0x9b, 0x54, 0x3c, 0x63, 0xde, 0x85, 0xe7, 0x33, 0x12, 0x59, 0x15, 0xc9, 0x2b, 0x57, 0x1d, 0xb6, 0xa8, 0x7c, 0x0a, 0xde, 0x59, 0x10, 0x32, 0x92, 0x38, 0x4d, 0xc5, 0xab, 0x56, 0xe8,
0xe8, 0x12, 0x40, 0xf0, 0x86, 0xd1, 0x9c, 0xdb, 0x0e, 0x04, 0x75, 0xbb, 0x00, 0xf5, 0xdb, 0x18, 0x12, 0x40, 0xf2, 0xc6, 0xc9, 0x94, 0xdb, 0xb6, 0x24, 0xf5, 0x59, 0x05, 0xea, 0xb7, 0x02, 0x8f,
0x8f, 0xeb, 0x54, 0xbf, 0xa2, 0x6f, 0xe1, 0x50, 0x4a, 0x32, 0x9d, 0x85, 0x73, 0x42, 0x2d, 0xf3, 0xdb, 0xd4, 0xbc, 0xa2, 0x6f, 0x61, 0x47, 0x49, 0x32, 0x9e, 0xc4, 0x53, 0x42, 0x1d, 0xeb, 0x59,
0x59, 0x99, 0x53, 0x5d, 0x48, 0x2a, 0xad, 0xf0, 0x44, 0x8a, 0x36, 0xe0, 0x08, 0xdc, 0x90, 0xf0, 0x83, 0x53, 0x9d, 0x2a, 0x2a, 0xa3, 0xf0, 0x48, 0x89, 0xd6, 0xe3, 0x08, 0xbc, 0xad, 0xe0, 0xe2,
0xf8, 0x9d, 0x3a, 0xbf, 0x40, 0x4d, 0xd3, 0x3b, 0x3d, 0x30, 0x65, 0xf0, 0xa8, 0x01, 0xd5, 0xeb, 0x9d, 0x7a, 0xbf, 0x80, 0x6d, 0xe8, 0xbd, 0x2e, 0x58, 0x2a, 0x78, 0xb4, 0x0d, 0xad, 0xeb, 0xe1,
0xf1, 0x9b, 0xf1, 0xdb, 0x9f, 0xc6, 0xcd, 0x27, 0xa8, 0x06, 0x95, 0x71, 0xff, 0xfb, 0x61, 0xd3, 0x9b, 0xe1, 0xdb, 0x9f, 0x86, 0xfb, 0x4f, 0x90, 0x0d, 0xcd, 0xe1, 0xc5, 0xf7, 0xfd, 0xfd, 0x1a,
0x40, 0x27, 0x70, 0x34, 0xea, 0x4f, 0x7e, 0x98, 0xe2, 0xe1, 0x68, 0xd8, 0x9f, 0x0c, 0x5f, 0x35, 0x3a, 0x80, 0xdd, 0xc1, 0xc5, 0xe8, 0x87, 0x31, 0xee, 0x0f, 0xfa, 0x17, 0xa3, 0xfe, 0xab, 0xfd,
0x4b, 0xce, 0x47, 0x50, 0x4f, 0xa2, 0x42, 0x55, 0x28, 0xf7, 0x27, 0x03, 0x79, 0xe4, 0xd5, 0x90, 0xba, 0xf7, 0x11, 0xb4, 0xd3, 0xa8, 0x50, 0x0b, 0x1a, 0x17, 0xa3, 0x9e, 0x3a, 0xf2, 0xaa, 0xcf,
0xbf, 0x19, 0xce, 0x9f, 0x06, 0x9c, 0xa6, 0x8b, 0x40, 0x57, 0x61, 0x40, 0x49, 0x5c, 0x85, 0x59, 0xdf, 0x6a, 0xde, 0x9f, 0x35, 0x38, 0xcc, 0x16, 0x81, 0x2e, 0xe2, 0x88, 0x12, 0x51, 0x85, 0x49,
0xb8, 0x0e, 0x92, 0x2a, 0x88, 0x05, 0x42, 0x50, 0x09, 0xc8, 0x1f, 0xba, 0x06, 0xe2, 0x3d, 0x46, 0xbc, 0x8c, 0xd2, 0x2a, 0xc8, 0x05, 0x42, 0xd0, 0x8c, 0xc8, 0x1f, 0xa6, 0x06, 0xf2, 0x5d, 0x20,
0xb2, 0x90, 0xb9, 0xbe, 0xd0, 0x9f, 0x23, 0xc5, 0x02, 0x7d, 0x0d, 0x35, 0x95, 0x1c, 0xe5, 0xca, 0x59, 0xcc, 0xfc, 0x50, 0xea, 0xcf, 0x91, 0x72, 0x81, 0xbe, 0x06, 0x5b, 0x27, 0x47, 0xb9, 0xb2,
0x96, 0xdb, 0x8d, 0xde, 0x59, 0x3a, 0x65, 0xe5, 0x11, 0x27, 0x30, 0xe7, 0x12, 0x5a, 0x97, 0x44, 0x8d, 0xb3, 0xed, 0xee, 0x51, 0x36, 0x65, 0xed, 0x11, 0xa7, 0x30, 0xef, 0x12, 0x4e, 0x2e, 0x89,
0x47, 0x22, 0x15, 0xd1, 0x77, 0x22, 0xf6, 0xeb, 0xde, 0x12, 0x11, 0x4c, 0xec, 0x97, 0xbf, 0x23, 0x89, 0x44, 0x29, 0x62, 0xee, 0x84, 0xf0, 0xeb, 0xdf, 0x12, 0x19, 0x8c, 0xf0, 0xcb, 0xdf, 0x91,
0x0b, 0xaa, 0xea, 0x42, 0x89, 0x70, 0x0e, 0xb0, 0x5e, 0x3a, 0x0c, 0xac, 0x87, 0x44, 0x2a, 0xaf, 0x03, 0x2d, 0x7d, 0xa1, 0x64, 0x38, 0x5b, 0xd8, 0x2c, 0x3d, 0x06, 0xce, 0x43, 0x22, 0x9d, 0x57,
0x2c, 0xa6, 0x4f, 0xa1, 0x12, 0x5f, 0x67, 0x41, 0xd3, 0xe8, 0xa1, 0x74, 0x9c, 0x57, 0xdc, 0x82, 0x11, 0xd3, 0xa7, 0xd0, 0x14, 0xd7, 0x59, 0xd2, 0x6c, 0x77, 0x51, 0x36, 0xce, 0x2b, 0x6e, 0xc1,
0x85, 0x1d, 0x7d, 0x00, 0xf5, 0x18, 0x4f, 0x57, 0xee, 0x8c, 0x88, 0x6c, 0xeb, 0xf8, 0x7e, 0xc3, 0xd2, 0x8e, 0x3e, 0x80, 0xb6, 0xc0, 0xd3, 0x85, 0x3f, 0x21, 0x32, 0xdb, 0x36, 0xbe, 0xdf, 0xf0,
0x79, 0xbd, 0xed, 0x75, 0x10, 0x06, 0x8c, 0x04, 0x6c, 0xbf, 0xf8, 0x47, 0x70, 0x91, 0xc1, 0xa4, 0x5e, 0xaf, 0x7b, 0xed, 0xc5, 0x11, 0x23, 0x11, 0xdb, 0x2c, 0xfe, 0x01, 0x9c, 0x16, 0x30, 0xe9,
0x12, 0xe8, 0x42, 0x55, 0x85, 0x26, 0xd8, 0x72, 0x75, 0xd5, 0x28, 0xe7, 0x1f, 0x5e, 0xe2, 0xeb, 0x04, 0xce, 0xa1, 0xa5, 0x43, 0x93, 0x6c, 0xa5, 0xba, 0x1a, 0x94, 0xf7, 0x2f, 0x2f, 0xf1, 0xf5,
0xd5, 0xdc, 0x65, 0x44, 0x9b, 0xfe, 0x27, 0xa8, 0xe7, 0xbc, 0xec, 0xf1, 0x58, 0x50, 0x5a, 0x9c, 0x62, 0xea, 0x33, 0x62, 0x4c, 0xff, 0x13, 0xd4, 0x73, 0x5e, 0x76, 0x31, 0x16, 0xb4, 0x16, 0x07,
0x48, 0x6e, 0x39, 0x3b, 0x06, 0xf1, 0x2f, 0x96, 0x76, 0xf4, 0x02, 0xcc, 0x8d, 0xeb, 0x73, 0x1e, 0x8a, 0x5b, 0xcd, 0x8e, 0x9e, 0xf8, 0xc5, 0xca, 0x8e, 0x5e, 0x80, 0xb5, 0xf2, 0x43, 0xce, 0x23,
0x21, 0x44, 0xa2, 0x9a, 0x42, 0x8a, 0x99, 0x82, 0x15, 0x02, 0xb5, 0xa0, 0x3a, 0x8f, 0xee, 0xa6, 0x85, 0x48, 0x55, 0xd3, 0x48, 0x39, 0x53, 0xb0, 0x46, 0xa0, 0x13, 0x68, 0x4d, 0x93, 0xbb, 0x71,
0xd1, 0x3a, 0x10, 0x4d, 0x56, 0xc3, 0x26, 0x5f, 0xe2, 0x75, 0x80, 0x3e, 0x86, 0xa3, 0xb9, 0x47, 0xb2, 0x8c, 0x64, 0x93, 0xd9, 0xd8, 0xe2, 0x4b, 0xbc, 0x8c, 0xd0, 0xc7, 0xb0, 0x3b, 0x0d, 0xa8,
0xdd, 0x1b, 0x9f, 0x4c, 0x97, 0x61, 0xf8, 0x1b, 0x15, 0x7d, 0x56, 0xc3, 0x87, 0x6a, 0xf3, 0x75, 0x7f, 0x13, 0x92, 0xf1, 0x3c, 0x8e, 0x7f, 0xa3, 0xb2, 0xcf, 0x6c, 0xbc, 0xa3, 0x37, 0x5f, 0x8b,
0xbc, 0xc7, 0x75, 0x3d, 0xdb, 0x09, 0x7f, 0x5f, 0x25, 0xfe, 0x35, 0xe0, 0xec, 0x2a, 0xe0, 0xed, 0x3d, 0xe4, 0x82, 0xcd, 0x79, 0x02, 0x91, 0x00, 0x6f, 0x1e, 0x61, 0x4f, 0xd7, 0x5c, 0xf3, 0xa3,
0xe5, 0xfb, 0x3b, 0x52, 0x24, 0x69, 0x1b, 0x85, 0xd3, 0x2e, 0xbd, 0x4f, 0xda, 0xe5, 0x54, 0xda, 0x5c, 0x6a, 0x9b, 0xaa, 0xf4, 0x57, 0x1d, 0x8e, 0xae, 0x22, 0xde, 0x7a, 0x61, 0x98, 0x93, 0x29,
0x5a, 0xf8, 0xca, 0x96, 0xf0, 0x45, 0xa4, 0x48, 0x5f, 0x40, 0x73, 0xe7, 0x02, 0xa2, 0x0f, 0x01, 0x95, 0xa4, 0x56, 0x59, 0x92, 0xfa, 0xfb, 0x48, 0xd2, 0xc8, 0x48, 0x62, 0x8a, 0xd2, 0x5c, 0x2b,
0x22, 0xb2, 0xa6, 0x64, 0x2a, 0xc8, 0xab, 0xe2, 0x7c, 0x5d, 0xec, 0x8c, 0xf9, 0x86, 0x73, 0x05, 0x4a, 0x25, 0x99, 0x32, 0x97, 0xd3, 0xca, 0x5d, 0x4e, 0xf4, 0x21, 0x40, 0x42, 0x96, 0x94, 0x8c,
0xe7, 0xbb, 0xc9, 0xef, 0x2b, 0xe4, 0x12, 0x5a, 0xd7, 0x81, 0x97, 0xa9, 0x64, 0xd6, 0xa5, 0x7a, 0x25, 0x79, 0x4b, 0x9e, 0x6f, 0xcb, 0x9d, 0xa1, 0xf0, 0xb0, 0xae, 0xb1, 0x9d, 0xd3, 0xf8, 0x0a,
0x90, 0x5b, 0x29, 0x23, 0x37, 0x3e, 0x46, 0x56, 0xeb, 0xe8, 0x1d, 0x51, 0x5a, 0xc9, 0x85, 0xf3, 0x8e, 0xf3, 0xc2, 0x6c, 0x2a, 0xf2, 0x1c, 0x4e, 0xae, 0xa3, 0xa0, 0x50, 0xe5, 0xa2, 0xcb, 0xf8,
0x06, 0xac, 0x87, 0x9e, 0xf6, 0x0d, 0xfb, 0x29, 0x9c, 0xf0, 0xbe, 0xfa, 0x51, 0x76, 0x99, 0x0a, 0x20, 0xef, 0x7a, 0x41, 0xde, 0x7c, 0xfc, 0x2c, 0x96, 0xc9, 0x3b, 0xa2, 0x75, 0x54, 0x0b, 0xef,
0xd8, 0x19, 0x02, 0xda, 0xde, 0xbc, 0xe7, 0x56, 0x5b, 0x69, 0x6e, 0xfd, 0x09, 0xd3, 0x78, 0xdd, 0x0d, 0x38, 0x0f, 0x3d, 0x6d, 0x1a, 0xf6, 0x53, 0x38, 0xe0, 0xfd, 0xf8, 0xa3, 0xea, 0x4e, 0x1d,
0xb3, 0xbd, 0xbf, 0x4c, 0x38, 0xd6, 0x13, 0x47, 0x7e, 0x1f, 0x90, 0x07, 0x87, 0xdb, 0xa3, 0x15, 0xb0, 0xd7, 0x07, 0xb4, 0xbe, 0x79, 0xcf, 0xad, 0xb7, 0xb2, 0xdc, 0xe6, 0xd3, 0x67, 0xf0, 0xa6,
0x7d, 0x96, 0xff, 0xf9, 0xd8, 0xf9, 0x06, 0xda, 0x2f, 0x8a, 0x40, 0x65, 0xa8, 0xce, 0x93, 0xaf, 0xd7, 0xbb, 0x7f, 0x5b, 0xb0, 0x67, 0x26, 0x95, 0xfa, 0xae, 0xa0, 0x00, 0x76, 0xd6, 0x47, 0x32,
0x0c, 0x44, 0xa1, 0xb9, 0x3b, 0xf1, 0xd0, 0x97, 0xd9, 0x1c, 0x39, 0x23, 0xd6, 0xee, 0x14, 0x85, 0xfa, 0xac, 0xfc, 0xb3, 0x93, 0xfb, 0x76, 0xba, 0x2f, 0xaa, 0x40, 0x55, 0xa8, 0xde, 0x93, 0xaf,
0x6b, 0xb7, 0x68, 0x23, 0xe4, 0x4c, 0x8f, 0x29, 0xf4, 0x28, 0x4d, 0x7a, 0x32, 0xda, 0xdd, 0xc2, 0x6a, 0x88, 0xc2, 0x7e, 0x7e, 0x52, 0xa2, 0x2f, 0x8b, 0x39, 0x4a, 0x46, 0xb3, 0xdb, 0xa9, 0x0a,
0xf8, 0xc4, 0xef, 0xaf, 0x70, 0x94, 0x1a, 0x08, 0x28, 0x47, 0xad, 0xac, 0xa1, 0x67, 0x7f, 0x5e, 0x37, 0x6e, 0xd1, 0x4a, 0xca, 0x99, 0x1d, 0x6f, 0xe8, 0x51, 0x9a, 0xec, 0x44, 0x75, 0xcf, 0x2b,
0x08, 0x9b, 0xf8, 0xba, 0x85, 0xe3, 0x74, 0xd3, 0xa0, 0x1c, 0x82, 0xcc, 0xb9, 0x62, 0x7f, 0x51, 0xe3, 0x53, 0xbf, 0xbf, 0xc2, 0x6e, 0x66, 0x58, 0xa0, 0x12, 0xb5, 0x8a, 0x86, 0xa5, 0xfb, 0x79,
0x0c, 0x9c, 0xb8, 0xe3, 0x75, 0xdc, 0xbd, 0xee, 0x79, 0x75, 0xcc, 0x69, 0xc0, 0xbc, 0x3a, 0xe6, 0x25, 0x6c, 0xea, 0xeb, 0x16, 0xf6, 0xb2, 0x4d, 0x83, 0x4a, 0x08, 0x0a, 0x67, 0x8e, 0xfb, 0x45,
0x75, 0x11, 0x77, 0xea, 0x02, 0xdc, 0x77, 0x00, 0x7a, 0x9e, 0x5b, 0x90, 0x74, 0xe3, 0xd8, 0xed, 0x35, 0x70, 0xea, 0x8e, 0xd7, 0x31, 0x7f, 0xdd, 0xcb, 0xea, 0x58, 0xd2, 0x80, 0x65, 0x75, 0x2c,
0xc7, 0x81, 0xda, 0xc5, 0x4b, 0xf8, 0xb9, 0xa6, 0x71, 0x37, 0xa6, 0xf8, 0xfb, 0xf7, 0xcd, 0x7f, 0xeb, 0x22, 0xee, 0xd4, 0x07, 0xb8, 0xef, 0x00, 0xf4, 0xbc, 0xb4, 0x20, 0xd9, 0xc6, 0x71, 0xcf,
0x01, 0x00, 0x00, 0xff, 0xff, 0x14, 0xb2, 0x9f, 0x07, 0xcf, 0x0a, 0x00, 0x00, 0x1e, 0x07, 0x1a, 0x17, 0x2f, 0xe1, 0x67, 0xdb, 0xe0, 0x6e, 0x2c, 0xf9, 0xb7, 0xf1, 0x9b, 0xff,
0x02, 0x00, 0x00, 0xff, 0xff, 0x7b, 0x5d, 0x13, 0x1a, 0x07, 0x0b, 0x00, 0x00,
} }

Loading…
Cancel
Save