New Chart linter structure

Fixed tests

Adding chart name linter

Add lint error

Moving to blocks

Moving to method

Moved lint rules to functions

Semantic version validation

Linting engine

Adding sources and home validations

Sharing file loading

Sharing file loading

Rolling back readme

Rewriting other linters

Fixing tests

Typo

Using chart.Metadata

Fixing format

Adding UNKNOWN in Engine

Adding tabs

Fixing tabs
pull/833/head
Miguel Martinez 9 years ago
parent 7a227440f7
commit c2459c06bf

@ -17,6 +17,10 @@ message Maintainer {
//
// Spec: https://k8s.io/helm/blob/master/docs/design/chart_format.md#the-chart-file
message Metadata {
enum Engine {
UNKNOWN = 0;
GOTPL = 1;
}
// The name of the chart
string name = 1;

@ -45,6 +45,11 @@ func lintCmd(cmd *cobra.Command, args []string) error {
}
issues := lint.All(path)
if len(issues) == 0 {
fmt.Println("Lint OK")
}
for _, i := range issues {
fmt.Printf("%s\n", i)
}

@ -4,5 +4,3 @@ version: 0.1.0
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm

2
glide.lock generated

@ -5,6 +5,8 @@ imports:
version: 75cd24fc2f2c
- name: github.com/aokoli/goutils
version: 9c37978a95bd5c709a15883b6242714ea6709e64
- name: github.com/asaskevich/govalidator
version: df81827fdd59d8b4fb93d8910b286ab7a3919520
- name: github.com/beorn7/perks
version: b965b613227fddccbfffe13eae360ed3fa822f8d
subpackages:

@ -38,3 +38,4 @@ import:
- package: speter.net/go/exp/math/dec/inf
repo: https://github.com/go-inf/inf.git
vcs: git
- package: github.com/asaskevich/govalidator

@ -1,46 +0,0 @@
package lint
import (
"os"
"path/filepath"
"k8s.io/helm/pkg/chartutil"
)
// Chartfile checks the Chart.yaml file for errors and warnings.
func Chartfile(basepath string) (m []Message) {
m = []Message{}
path := filepath.Join(basepath, "Chart.yaml")
if fi, err := os.Stat(path); err != nil {
m = append(m, Message{Severity: ErrorSev, Text: "Chart.yaml file: " + path + " does not exist"})
return
} else if fi.IsDir() {
m = append(m, Message{Severity: ErrorSev, Text: "Chart.yaml is a directory."})
return
}
cf, err := chartutil.LoadChartfile(path)
if err != nil {
m = append(m, Message{
Severity: ErrorSev,
Text: err.Error(),
})
return
}
if cf.Name == "" {
m = append(m, Message{
Severity: ErrorSev,
Text: "Chart.yaml: 'name' is required",
})
}
if cf.Version == "" || cf.Version == "0.0.0" {
m = append(m, Message{
Severity: ErrorSev,
Text: "Chart.yaml: 'version' is required, and must be greater than 0.0.0",
})
}
return
}

@ -1,22 +0,0 @@
package lint
import (
"testing"
)
const badchartfile = "testdata/badchartfile"
func TestChartfile(t *testing.T) {
msgs := Chartfile(badchartfile)
if len(msgs) != 2 {
t.Errorf("Expected 2 errors, got %d", len(msgs))
}
if msgs[0].Text != "Chart.yaml: 'name' is required" {
t.Errorf("Unexpected message 0: %s", msgs[0].Text)
}
if msgs[1].Text != "Chart.yaml: 'version' is required, and must be greater than 0.0.0" {
t.Errorf("Unexpected message 1: %s", msgs[1].Text)
}
}

@ -1,9 +1,21 @@
package lint
import (
"k8s.io/helm/pkg/lint/rules"
"k8s.io/helm/pkg/lint/support"
"os"
"path/filepath"
)
// All runs all of the available linters on the given base directory.
func All(basedir string) []Message {
out := Chartfile(basedir)
out = append(out, Templates(basedir)...)
out = append(out, Values(basedir)...)
return out
func All(basedir string) []support.Message {
// Using abs path to get directory context
current, _ := os.Getwd()
chartDir := filepath.Join(current, basedir)
linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter)
rules.Values(&linter)
rules.Templates(&linter)
return linter.Messages
}

@ -1,39 +1,44 @@
package lint
import (
"k8s.io/helm/pkg/lint/support"
"strings"
"testing"
)
const badChartDir = "testdata/badchartfile"
const badValuesFileDir = "testdata/badvaluesfile"
const badYamlFileDir = "testdata/albatross"
const goodChartDir = "testdata/goodone"
const badChartDir = "rules/testdata/badchartfile"
const badValuesFileDir = "rules/testdata/badvaluesfile"
const badYamlFileDir = "rules/testdata/albatross"
const goodChartDir = "rules/testdata/goodone"
func TestBadChart(t *testing.T) {
m := All(badChartDir)
if len(m) != 3 {
if len(m) != 4 {
t.Errorf("Number of errors %v", len(m))
t.Errorf("All didn't fail with expected errors, got %#v", m)
}
// There should be 2 WARNINGs and one ERROR messages, check for them
var w, e, e2 = false, false, false
var w, e, e2, e3 bool
for _, msg := range m {
if msg.Severity == WarningSev {
if strings.Contains(msg.Text, "No templates") {
if msg.Severity == support.WarningSev {
if strings.Contains(msg.Text, "Templates directory not found") {
w = true
}
}
if msg.Severity == ErrorSev {
if strings.Contains(msg.Text, "must be greater than 0.0.0") {
if msg.Severity == support.ErrorSev {
if strings.Contains(msg.Text, "'version' 0.0.0 is less than or equal to 0") {
e = true
}
if strings.Contains(msg.Text, "'name' is required") {
e2 = true
}
if strings.Contains(msg.Text, "'name' and directory do not match") {
e3 = true
}
}
}
if !e || !e2 || !w {
if !e || !e2 || !e3 || !w {
t.Errorf("Didn't find all the expected errors, got %#v", m)
}
}

@ -0,0 +1,153 @@
package rules
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver"
"github.com/asaskevich/govalidator"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support"
"k8s.io/helm/pkg/proto/hapi/chart"
)
// Chartfile runs a set of linter rules related to Chart.yaml file
func Chartfile(linter *support.Linter) {
chartPath := filepath.Join(linter.ChartDir, "Chart.yaml")
linter.RunLinterRule(support.ErrorSev, validateChartYamlFileExistence(linter, chartPath))
linter.RunLinterRule(support.ErrorSev, validateChartYamlNotDirectory(linter, chartPath))
chartFile, err := chartutil.LoadChartfile(chartPath)
validChartFile := linter.RunLinterRule(support.ErrorSev, validateChartYamlFormat(linter, err))
// Guard clause. Following linter rules require a parseable ChartFile
if !validChartFile {
return
}
linter.RunLinterRule(support.ErrorSev, validateChartName(linter, chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartNameDirMatch(linter, chartFile))
// Chart metadata
linter.RunLinterRule(support.ErrorSev, validateChartVersion(linter, chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartEngine(linter, chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartMaintainer(linter, chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartSources(linter, chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartHome(linter, chartFile))
}
// Auxiliar validation methods
func validateChartYamlFileExistence(linter *support.Linter, chartPath string) (lintError support.LintError) {
_, err := os.Stat(chartPath)
if err != nil {
lintError = fmt.Errorf("Chart.yaml file does not exists")
}
return
}
func validateChartYamlNotDirectory(linter *support.Linter, chartPath string) (lintError support.LintError) {
fi, err := os.Stat(chartPath)
if err == nil && fi.IsDir() {
lintError = fmt.Errorf("Chart.yaml is a directory")
}
return
}
func validateChartYamlFormat(linter *support.Linter, chartFileError error) (lintError support.LintError) {
if chartFileError != nil {
lintError = fmt.Errorf("Chart.yaml is malformed: %s", chartFileError.Error())
}
return
}
func validateChartName(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
if cf.Name == "" {
lintError = fmt.Errorf("Chart.yaml: 'name' is required")
}
return
}
func validateChartNameDirMatch(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
if cf.Name != filepath.Base(linter.ChartDir) {
lintError = fmt.Errorf("Chart.yaml: 'name' and directory do not match")
}
return
}
func validateChartVersion(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
if cf.Version == "" {
lintError = fmt.Errorf("Chart.yaml: 'version' value is required")
return
}
version, err := semver.NewVersion(cf.Version)
if err != nil {
lintError = fmt.Errorf("Chart.yaml: version '%s' is not a valid SemVer", cf.Version)
return
}
c, err := semver.NewConstraint("> 0")
valid, msg := c.Validate(version)
if !valid && len(msg) > 0 {
lintError = fmt.Errorf("Chart.yaml: 'version' %v", msg[0])
}
return
}
func validateChartEngine(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
if cf.Engine == "" {
return
}
keys := make([]string, 0, len(chart.Metadata_Engine_value))
for engine := range chart.Metadata_Engine_value {
str := strings.ToLower(engine)
if str == "unknown" {
continue
}
if str == cf.Engine {
return
}
keys = append(keys, str)
}
lintError = fmt.Errorf("Chart.yaml: 'engine %v not valid. Valid options are %v", cf.Engine, keys)
return
}
func validateChartMaintainer(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
for _, maintainer := range cf.Maintainers {
if maintainer.Name == "" {
lintError = fmt.Errorf("Chart.yaml: maintainer requires a name")
} else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) {
lintError = fmt.Errorf("Chart.yaml: maintainer invalid email")
}
}
return
}
func validateChartSources(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
for _, source := range cf.Sources {
if source == "" || !govalidator.IsRequestURL(source) {
lintError = fmt.Errorf("Chart.yaml: 'source' invalid URL %s", source)
}
}
return
}
func validateChartHome(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
if cf.Home != "" && !govalidator.IsRequestURL(cf.Home) {
lintError = fmt.Errorf("Chart.yaml: 'home' invalid URL %s", cf.Home)
}
return
}

@ -0,0 +1,31 @@
package rules
import (
"k8s.io/helm/pkg/lint/support"
"strings"
"testing"
)
const badchartfile = "testdata/badchartfile"
func TestChartfile(t *testing.T) {
linter := support.Linter{ChartDir: badchartfile}
Chartfile(&linter)
msgs := linter.Messages
if len(msgs) != 3 {
t.Errorf("Expected 3 errors, got %d", len(msgs))
}
if !strings.Contains(msgs[0].Text, "'name' is required") {
t.Errorf("Unexpected message 0: %s", msgs[0].Text)
}
if !strings.Contains(msgs[1].Text, "'name' and directory do not match") {
t.Errorf("Unexpected message 1: %s", msgs[1].Text)
}
if !strings.Contains(msgs[2].Text, "'version' 0.0.0 is less than or equal to 0") {
t.Errorf("Unexpected message 2: %s", msgs[2].Text)
}
}

@ -0,0 +1,70 @@
package rules
import (
"fmt"
"github.com/Masterminds/sprig"
"io/ioutil"
"k8s.io/helm/pkg/lint/support"
"os"
"path/filepath"
"text/template"
)
// Templates lints a chart's templates.
func Templates(linter *support.Linter) {
templatespath := filepath.Join(linter.ChartDir, "templates")
templatesExist := linter.RunLinterRule(support.WarningSev, validateTemplatesExistence(linter, templatespath))
// Templates directory is optional for now
if !templatesExist {
return
}
linter.RunLinterRule(support.ErrorSev, validateTemplatesDir(linter, templatespath))
linter.RunLinterRule(support.ErrorSev, validateTemplatesParseable(linter, templatespath))
}
func validateTemplatesExistence(linter *support.Linter, templatesPath string) (lintError support.LintError) {
if _, err := os.Stat(templatesPath); err != nil {
lintError = fmt.Errorf("Templates directory not found")
}
return
}
func validateTemplatesDir(linter *support.Linter, templatesPath string) (lintError support.LintError) {
fi, err := os.Stat(templatesPath)
if err == nil && !fi.IsDir() {
lintError = fmt.Errorf("'templates' is not a directory")
}
return
}
func validateTemplatesParseable(linter *support.Linter, templatesPath string) (lintError support.LintError) {
tpl := template.New("tpl").Funcs(sprig.TxtFuncMap())
lintError = filepath.Walk(templatesPath, func(name string, fi os.FileInfo, e error) error {
if e != nil {
return e
}
if fi.IsDir() {
return nil
}
data, err := ioutil.ReadFile(name)
if err != nil {
lintError = fmt.Errorf("cannot read %s: %s", name, err)
return lintError
}
newtpl, err := tpl.Parse(string(data))
if err != nil {
lintError = fmt.Errorf("error processing %s: %s", name, err)
return lintError
}
tpl = newtpl
return nil
})
return
}

@ -1,6 +1,7 @@
package lint
package rules
import (
"k8s.io/helm/pkg/lint/support"
"strings"
"testing"
)
@ -8,10 +9,12 @@ import (
const templateTestBasedir = "./testdata/albatross"
func TestTemplate(t *testing.T) {
res := Templates(templateTestBasedir)
linter := support.Linter{ChartDir: templateTestBasedir}
Templates(&linter)
res := linter.Messages
if len(res) != 1 {
t.Fatalf("Expected one error, got %d", len(res))
t.Fatalf("Expected one error, got %d, %v", len(res), res)
}
if !strings.Contains(res[0].Text, "deliberateSyntaxError") {

@ -0,0 +1,37 @@
package rules
import (
"fmt"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support"
"os"
"path/filepath"
)
// Values lints a chart's values.yaml file.
func Values(linter *support.Linter) {
vf := filepath.Join(linter.ChartDir, "values.yaml")
fileExists := linter.RunLinterRule(support.InfoSev, validateValuesFileExistence(linter, vf))
if !fileExists {
return
}
linter.RunLinterRule(support.ErrorSev, validateValuesFile(linter, vf))
}
func validateValuesFileExistence(linter *support.Linter, valuesPath string) (lintError support.LintError) {
_, err := os.Stat(valuesPath)
if err != nil {
lintError = fmt.Errorf("values.yaml file does not exists")
}
return
}
func validateValuesFile(linter *support.Linter, valuesPath string) (lintError support.LintError) {
_, err := chartutil.ReadValuesFile(valuesPath)
if err != nil {
lintError = fmt.Errorf("values.yaml is malformed: %s", err.Error())
}
return
}

@ -3,4 +3,4 @@
Linting is the process of testing charts for errors or warnings regarding
formatting, compilation, or standards compliance.
*/
package lint
package support

@ -1,4 +1,4 @@
package lint
package support
import "fmt"
@ -22,14 +22,33 @@ var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"}
// Message is a linting output message
type Message struct {
// Severity is one of the *Sev constants
Severity int
Severity Severity
// Text contains the message text
Text string
}
type Linter struct {
Messages []Message
ChartDir string
}
type LintError interface {
error
}
type ValidationFunc func(*Linter) LintError
// String prints a string representation of this Message.
//
// Implements fmt.Stringer.
func (m Message) String() string {
return fmt.Sprintf("[%s] %s", sev[m.Severity], m.Text)
}
// Returns true if the validation passed
func (l *Linter) RunLinterRule(severity Severity, lintError LintError) bool {
if lintError != nil {
l.Messages = append(l.Messages, Message{Text: lintError.Error(), Severity: severity})
}
return lintError == nil
}

@ -1,4 +1,4 @@
package lint
package support
import (
"fmt"

@ -1,64 +0,0 @@
package lint
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"text/template"
"github.com/Masterminds/sprig"
)
// Templates lints a chart's templates.
func Templates(basepath string) (messages []Message) {
messages = []Message{}
path := filepath.Join(basepath, "templates")
if fi, err := os.Stat(path); err != nil {
messages = append(messages, Message{Severity: WarningSev, Text: "No templates"})
return
} else if !fi.IsDir() {
messages = append(messages, Message{Severity: ErrorSev, Text: "'templates' is not a directory"})
return
}
tpl := template.New("tpl").Funcs(sprig.TxtFuncMap())
err := filepath.Walk(basepath, func(name string, fi os.FileInfo, e error) error {
// If an error is returned, we fail. Non-fatal errors should just be
// added directly to messages.
if e != nil {
return e
}
if fi.IsDir() {
return nil
}
data, err := ioutil.ReadFile(name)
if err != nil {
messages = append(messages, Message{
Severity: ErrorSev,
Text: fmt.Sprintf("cannot read %s: %s", name, err),
})
return nil
}
// An error rendering a file should emit a warning.
newtpl, err := tpl.Parse(string(data))
if err != nil {
messages = append(messages, Message{
Severity: ErrorSev,
Text: fmt.Sprintf("error processing %s: %s", name, err),
})
return nil
}
tpl = newtpl
return nil
})
if err != nil {
messages = append(messages, Message{Severity: ErrorSev, Text: err.Error()})
}
return
}

@ -1,23 +0,0 @@
package lint
import (
"os"
"path/filepath"
"k8s.io/helm/pkg/chartutil"
)
// Values lints a chart's values.yaml file.
func Values(basepath string) (messages []Message) {
vf := filepath.Join(basepath, "values.yaml")
messages = []Message{}
if _, err := os.Stat(vf); err != nil {
messages = append(messages, Message{Severity: InfoSev, Text: "No values.yaml file"})
return
}
_, err := chartutil.ReadValuesFile(vf)
if err != nil {
messages = append(messages, Message{Severity: ErrorSev, Text: err.Error()})
}
return messages
}

@ -13,6 +13,27 @@ var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
type Metadata_Engine int32
const (
Metadata_UNKNOWN Metadata_Engine = 0
Metadata_GOTPL Metadata_Engine = 1
)
var Metadata_Engine_name = map[int32]string{
0: "UNKNOWN",
1: "GOTPL",
}
var Metadata_Engine_value = map[string]int32{
"UNKNOWN": 0,
"GOTPL": 1,
}
func (x Metadata_Engine) String() string {
return proto.EnumName(Metadata_Engine_name, int32(x))
}
func (Metadata_Engine) EnumDescriptor() ([]byte, []int) { return fileDescriptor2, []int{1, 0} }
// Maintainer describes a Chart maintainer.
type Maintainer struct {
// Name is a user name or organization name
@ -63,23 +84,26 @@ func (m *Metadata) GetMaintainers() []*Maintainer {
func init() {
proto.RegisterType((*Maintainer)(nil), "hapi.chart.Maintainer")
proto.RegisterType((*Metadata)(nil), "hapi.chart.Metadata")
proto.RegisterEnum("hapi.chart.Metadata_Engine", Metadata_Engine_name, Metadata_Engine_value)
}
var fileDescriptor2 = []byte{
// 234 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0xbd, 0x4f, 0xc3, 0x40,
0x0c, 0xc5, 0x15, 0xda, 0x7c, 0xe0, 0x6c, 0x16, 0xaa, 0x0c, 0x53, 0xd4, 0x89, 0x29, 0x95, 0x40,
0x42, 0xcc, 0xec, 0x5d, 0x3a, 0xb2, 0x99, 0xc4, 0x22, 0x27, 0x48, 0x2e, 0xba, 0x3b, 0x40, 0xfc,
0xe3, 0xcc, 0x5c, 0xdc, 0xaf, 0x0c, 0x1d, 0x22, 0xbd, 0xf7, 0x7e, 0x79, 0x3e, 0xd9, 0x70, 0xdb,
0xf1, 0x68, 0x36, 0x4d, 0xc7, 0x2e, 0x6c, 0x7a, 0x09, 0xdc, 0x72, 0xe0, 0x7a, 0x74, 0x36, 0x58,
0x84, 0x09, 0xd5, 0x8a, 0xd6, 0x4f, 0x00, 0x5b, 0x36, 0x43, 0x88, 0x9f, 0x38, 0x44, 0x58, 0x0e,
0xdc, 0x0b, 0x25, 0x55, 0x72, 0x7f, 0xbd, 0x53, 0x8d, 0x37, 0x90, 0x4a, 0xcf, 0xe6, 0x93, 0xae,
0x34, 0xdc, 0x9b, 0xf5, 0x5f, 0x02, 0xc5, 0xf6, 0x30, 0xf6, 0x62, 0x2d, 0x66, 0x9d, 0x8d, 0xd9,
0xbe, 0xa5, 0x1a, 0x09, 0x72, 0x6f, 0xbf, 0x5c, 0x23, 0x9e, 0x16, 0xd5, 0x22, 0xc6, 0x47, 0x3b,
0x91, 0x6f, 0x71, 0xde, 0xd8, 0x81, 0x96, 0x5a, 0x38, 0x5a, 0xac, 0xa0, 0x6c, 0xc5, 0x37, 0xce,
0x8c, 0x61, 0xa2, 0xa9, 0xd2, 0x79, 0x84, 0x77, 0x50, 0x7c, 0xc8, 0xef, 0x8f, 0x75, 0xad, 0xa7,
0x4c, 0xc7, 0x9e, 0x3c, 0x3e, 0x43, 0xd9, 0x9f, 0xd6, 0xf3, 0x94, 0x47, 0x5c, 0x3e, 0xac, 0xea,
0xf3, 0x01, 0xea, 0xf3, 0xf6, 0xbb, 0xf9, 0xaf, 0xb8, 0x82, 0x4c, 0x86, 0xf7, 0xa8, 0xa9, 0xd0,
0x27, 0x0f, 0xee, 0x25, 0x7f, 0x4d, 0xb5, 0xf8, 0x96, 0xe9, 0x31, 0x1f, 0xff, 0x03, 0x00, 0x00,
0xff, 0xff, 0x6f, 0x4a, 0x7b, 0xd0, 0x69, 0x01, 0x00, 0x00,
// 266 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0x4b, 0x4b, 0xc4, 0x40,
0x10, 0x84, 0xdd, 0x47, 0x1e, 0xdb, 0xb9, 0x2c, 0x8d, 0x2c, 0xa3, 0xa7, 0x90, 0x93, 0xa7, 0x2c,
0x28, 0x88, 0x67, 0x41, 0x3c, 0xe8, 0x66, 0x65, 0x51, 0x04, 0x6f, 0x63, 0xd2, 0x98, 0x41, 0x93,
0x09, 0x33, 0xa3, 0xe2, 0x3f, 0xf1, 0xe7, 0x3a, 0xe9, 0x7d, 0x1e, 0x3c, 0x04, 0xaa, 0xea, 0x4b,
0xd7, 0xd0, 0x0d, 0x27, 0xb5, 0xec, 0xd4, 0xbc, 0xac, 0xa5, 0x71, 0xf3, 0x86, 0x9c, 0xac, 0xa4,
0x93, 0x79, 0x67, 0xb4, 0xd3, 0x08, 0x3d, 0xca, 0x19, 0x65, 0x97, 0x00, 0x0b, 0xa9, 0x5a, 0xe7,
0x3f, 0x32, 0x88, 0x30, 0x6e, 0x65, 0x43, 0x62, 0x90, 0x0e, 0xce, 0x26, 0x2b, 0xd6, 0x78, 0x0c,
0x01, 0x35, 0x52, 0x7d, 0x88, 0x21, 0x87, 0x6b, 0x93, 0xfd, 0x0e, 0x21, 0x5e, 0x6c, 0x6a, 0xff,
0x1d, 0xf3, 0x59, 0xad, 0x7d, 0xb6, 0x9e, 0x62, 0x8d, 0x02, 0x22, 0xab, 0x3f, 0x4d, 0x49, 0x56,
0x8c, 0xd2, 0x91, 0x8f, 0xb7, 0xb6, 0x27, 0x5f, 0x64, 0xac, 0xd2, 0xad, 0x18, 0xf3, 0xc0, 0xd6,
0x62, 0x0a, 0x49, 0x45, 0xb6, 0x34, 0xaa, 0x73, 0x3d, 0x0d, 0x98, 0x1e, 0x46, 0x78, 0x0a, 0xf1,
0x3b, 0xfd, 0x7c, 0x6b, 0x53, 0x59, 0x11, 0x72, 0xed, 0xce, 0xe3, 0x15, 0x24, 0xcd, 0x6e, 0x3d,
0x2b, 0x22, 0x8f, 0x93, 0xf3, 0x59, 0xbe, 0x3f, 0x40, 0xbe, 0xdf, 0x7e, 0x75, 0xf8, 0x2b, 0xce,
0x20, 0xa4, 0xf6, 0xcd, 0x6b, 0x11, 0xf3, 0x93, 0x1b, 0x97, 0xa5, 0x10, 0xde, 0xb0, 0xc2, 0x04,
0xa2, 0xa7, 0xe2, 0xae, 0x58, 0x3e, 0x17, 0xd3, 0x23, 0x9c, 0x40, 0x70, 0xbb, 0x7c, 0x7c, 0xb8,
0x9f, 0x0e, 0xae, 0xa3, 0x97, 0x80, 0xab, 0x5f, 0x43, 0x3e, 0xf7, 0xc5, 0x5f, 0x00, 0x00, 0x00,
0xff, 0xff, 0xe5, 0xf8, 0x57, 0xee, 0x8b, 0x01, 0x00, 0x00,
}

Loading…
Cancel
Save