-
Notifications
You must be signed in to change notification settings - Fork 78
/
http_state.go
168 lines (145 loc) · 4.92 KB
/
http_state.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package remote
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"kusionstack.io/kusion/pkg/engine/states"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
"kusionstack.io/kusion/pkg/log"
)
// HTTPState represent a remote state that can be requested by HTTP.
// This state is designed to provide a generic way to manipulate State in third-party services
//
// Some url formats are given to bring relative flexibility for third-party services to implement their own State HTTP service and these
// formats MUST contain 4 "%s" placeholders for tenant, project and stack, since we will replace this format with fmt.Sprintf()
// Let's get applyURLFormat as an example to demonstrate how this suffix format works.
//
//
// Example:
//
// urlPrefix = "http://kusionstack.io"
// applyURLFormat = "/apis/v1/tenants/%s/projects/%s/stacks/%s/clusters/%s/states/"
// tenant = "t"
// project = "p"
// stack = "s"
// cluster = "c"
// the final request URL = "http://kusionstack.io/apis/v1/tenants/t/projects/p/stacks/s/clusters/c/states"
type HTTPState struct {
// urlPrefix is the prefix added in front of all request URLs. e.g. "http://kusionstack.io/"
urlPrefix string
// applyURLFormat is the suffix url format to apply a state
applyURLFormat string
// getLatestURLFormat is the suffix url format to get the latest state
getLatestURLFormat string
}
// NewHTTPState builds a new HTTPState with ConfigSchema() and validates params with Configure()
func NewHTTPState(params map[string]interface{}) (*HTTPState, error) {
h := &HTTPState{}
ctyValue, err := gocty.ToCtyValue(params, (&HTTPState{}).ConfigSchema())
if err != nil {
return nil, err
}
if e := h.Configure(ctyValue); e != nil {
return nil, e
}
return h, nil
}
const ParamsCounts = 4
// ConfigSchema is an implementation of StateStorage.ConfigSchema
func (s *HTTPState) ConfigSchema() cty.Type {
config := map[string]cty.Type{
"urlPrefix": cty.String,
"applyURLFormat": cty.String,
"getLatestURLFormat": cty.String,
}
return cty.Object(config)
}
// Configure is an implementation of StateStorage.Configure
func (s *HTTPState) Configure(obj cty.Value) error {
var url cty.Value
if url = obj.GetAttr("urlPrefix"); url.IsNull() || url.AsString() == "" {
return errors.New("urlPrefix can not be empty")
} else {
s.urlPrefix = url.AsString()
}
if applyFormat := obj.GetAttr("applyURLFormat"); applyFormat.IsNull() || url.AsString() == "" {
return errors.New("applyURLFormat can not be empty")
} else {
asString := applyFormat.AsString()
count := strings.Count(asString, "%s")
if count != ParamsCounts {
return errors.New("applyURLFormat must contains 4 \"%s\" placeholders for tenant, project, " +
"stack and cluster. Current format:" + asString)
}
s.applyURLFormat = asString
}
if getLatest := obj.GetAttr("getLatestURLFormat"); getLatest.IsNull() && getLatest.AsString() == "" {
return errors.New("getLatestURLFormat can not be empty")
} else {
asString := getLatest.AsString()
count := strings.Count(asString, "%s")
if count != ParamsCounts {
return errors.New("getLatestURLFormat must contains 4 \"%s\" placeholders for tenant, project, " +
"stack or cluster. Current format:" + asString)
}
s.getLatestURLFormat = asString
}
return nil
}
// GetLatestState is an implementation of StateStorage.GetLatestState
func (s *HTTPState) GetLatestState(query *states.StateQuery) (*states.State, error) {
url := fmt.Sprintf("%s"+s.getLatestURLFormat, s.urlPrefix, query.Tenant, query.Project, query.Stack, query.Cluster)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == 404 {
log.Info("Can't find the latest state by request:%s", url)
return nil, nil
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("get the latest state failed. StatusCode:%v, Status:%s", res.StatusCode, res.Status)
}
state := &states.State{}
resBody, _ := ioutil.ReadAll(res.Body)
err = json.Unmarshal(resBody, state)
if err != nil {
return nil, err
}
return state, nil
}
// Apply is an implementation of StateStorage.Apply
func (s *HTTPState) Apply(state *states.State) error {
jsonState, err := json.Marshal(state)
if err != nil {
return err
}
url := fmt.Sprintf("%s"+s.applyURLFormat, s.urlPrefix, state.Tenant, state.Project, state.Stack, state.Cluster)
req, err := http.NewRequest("POST", url, strings.NewReader(string(jsonState)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if res.StatusCode != 200 {
return fmt.Errorf("apply state failed. StatusCode:%v, Status:%s", res.StatusCode, res.Status)
}
defer res.Body.Close()
return nil
}
// Delete is not support now
func (s *HTTPState) Delete(id string) error {
return errors.New("not supported")
}