Skip to content

Commit

Permalink
Merge branch 'master' into bug/issue-10960-mesheryctl-pattern-import-url
Browse files Browse the repository at this point in the history
  • Loading branch information
leecalcote committed May 25, 2024
2 parents 878a880 + d667dd5 commit 8a87a61
Show file tree
Hide file tree
Showing 31 changed files with 1,294 additions and 140 deletions.
2 changes: 1 addition & 1 deletion docs/_data/discuss/meshery.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/_data/discuss/mesheryctl.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ mesheryctl registry generate --registrant-def [path to connection definition] --
<div class='codeblock'>
-h, --help help for generate
-o, --output string location to output generated models, defaults to ../server/meshmodels (default "../server/meshmodel")
--registrant-cred string path pointing to the registrant credetial definition
--registrant-cred string path pointing to the registrant credential definition
--registrant-def string path pointing to the registrant connection definition
--spreadsheet-cred string base64 encoded credential to download the spreadsheet
--spreadsheet-id string spreadsheet it for the integration spreadsheet
--spreadsheet-id string spreadsheet ID for the integration spreadsheet

</div>
</pre>
Expand Down
10 changes: 5 additions & 5 deletions mesheryctl/internal/cli/root/registry/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func InvokeGenerationFromSheet(wg *sync.WaitGroup) error {
utils.Log.Error(ErrGenerateModel(err, model.Model))
return
}
utils.Log.Info("Extracted ", len(comps), "components for model [ %s ]", model.ModelDisplayName)
utils.Log.Info(" extracted ", len(comps), " components for ", model.ModelDisplayName, " (", model.Model, ")")

for _, comp := range comps {
comp.Version = defVersion
Expand Down Expand Up @@ -394,22 +394,22 @@ func writeModelDefToFileSystem(model *utils.ModelCSV, version, modelDefPath stri

func logModelGenerationSummary(modelToCompGenerateTracker *store.GenerticThreadSafeStore[compGenerateTracker]) {
for key, val := range modelToCompGenerateTracker.GetAllPairs() {
utils.Log.Info(fmt.Sprintf("For model %s-%s, generated %d components.", key, val.version, val.totalComps))
utils.Log.Info(fmt.Sprintf("Generated %d components for model [%s] %s", val.totalComps, key, val.version))
totalAggregateComponents += val.totalComps
totalAggregateModel++
}

utils.Log.Info(fmt.Sprintf("Generated %d models and %d components", totalAggregateModel, totalAggregateComponents))
utils.Log.Info(fmt.Sprintf("-----------------------------\n-----------------------------\nGenerated %d models and %d components", totalAggregateModel, totalAggregateComponents))
}

func init() {
generateCmd.PersistentFlags().StringVar(&spreadsheeetID, "spreadsheet-id", "", "spreadsheet it for the integration spreadsheet")
generateCmd.PersistentFlags().StringVar(&spreadsheeetID, "spreadsheet-id", "", "spreadsheet ID for the integration spreadsheet")
generateCmd.PersistentFlags().StringVar(&spreadsheeetCred, "spreadsheet-cred", "", "base64 encoded credential to download the spreadsheet")

generateCmd.MarkFlagsRequiredTogether("spreadsheet-id", "spreadsheet-cred")

generateCmd.PersistentFlags().StringVar(&pathToRegistrantConnDefinition, "registrant-def", "", "path pointing to the registrant connection definition")
generateCmd.PersistentFlags().StringVar(&pathToRegistrantCredDefinition, "registrant-cred", "", "path pointing to the registrant credetial definition")
generateCmd.PersistentFlags().StringVar(&pathToRegistrantCredDefinition, "registrant-cred", "", "path pointing to the registrant credential definition")

generateCmd.MarkFlagsRequiredTogether("registrant-def", "registrant-cred")

Expand Down
3 changes: 2 additions & 1 deletion mesheryctl/pkg/utils/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ func (mch *ComponentCSVHelper) ParseComponentsSheet() error {
}

go func() {
Log.Info(fmt.Sprintf("Parsing Components..."))

Check failure on line 153 in mesheryctl/pkg/utils/component.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.21, ubuntu-22.04)

S1039: unnecessary use of fmt.Sprintf (gosimple)

Check failure on line 153 in mesheryctl/pkg/utils/component.go

View workflow job for this annotation

GitHub Actions / golangci-lint

S1039: unnecessary use of fmt.Sprintf (gosimple)
err := csvReader.Parse(ch, errorChan)
if err != nil {
errorChan <- err
Expand All @@ -167,7 +168,7 @@ func (mch *ComponentCSVHelper) ParseComponentsSheet() error {
mch.Components[data.Registrant][data.Model] = make([]ComponentCSV, 0)
}
mch.Components[data.Registrant][data.Model] = append(mch.Components[data.Registrant][data.Model], data)
Log.Info(fmt.Sprintf("Reading Registrant [ %s ] Model [ %s ] Component [%s ]\n", data.Component, data.Model, data.Registrant))
Log.Info(fmt.Sprintf("Reading registrant [%s] model [%s] component [%s]", data.Registrant, data.Model, data.Component))
case err := <-errorChan:
Log.Error(err)

Expand Down
3 changes: 2 additions & 1 deletion mesheryctl/pkg/utils/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ func (mch *ModelCSVHelper) ParseModelsSheet(parseForDocs bool) error {
}

go func() {
Log.Info(fmt.Sprintf("Parsing Models..."))

Check failure on line 176 in mesheryctl/pkg/utils/model.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.21, ubuntu-22.04)

S1039: unnecessary use of fmt.Sprintf (gosimple)

Check failure on line 176 in mesheryctl/pkg/utils/model.go

View workflow job for this annotation

GitHub Actions / golangci-lint

S1039: unnecessary use of fmt.Sprintf (gosimple)
err := csvReader.Parse(ch, errorChan)
if err != nil {
errorChan <- err
Expand All @@ -183,7 +184,7 @@ func (mch *ModelCSVHelper) ParseModelsSheet(parseForDocs bool) error {

case data := <-ch:
mch.Models = append(mch.Models, data)
Log.Info(fmt.Sprintf("Reading Model [ %s ] from Registrant [ %s ] \n", data.Model, data.Registrant))
Log.Info(fmt.Sprintf("Reading registrant [%s] model [%s]", data.Registrant, data.Model))
case err := <-errorChan:
return ErrFileRead(err)

Expand Down
201 changes: 77 additions & 124 deletions server/handlers/meshery_pattern_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handlers

import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -697,145 +698,67 @@ func (h *Handler) handlePatternPOST(

}

func (h *Handler) HandleConversionToDesign(
rw http.ResponseWriter,
r *http.Request,
_ *models.Preference,
user *models.User,
provider models.Provider,
) {
defer func() {
_ = r.Body.Close()
}()

userUUID := uuid.FromStringOrNil(user.ID)
eventBuilder := events.NewEvent().FromSystem(*h.SystemID).FromUser(userUUID).WithCategory("pattern").WithAction("convert").ActedUpon(userUUID)

sourcetype := mux.Vars(r)["sourcetype"]
parsedBody := &MesheryPatternPOSTRequestBody{}
if err := json.NewDecoder(r.Body).Decode(&parsedBody); err != nil {
event := eventBuilder.WithDescription(fmt.Sprintf("Unable to convert design of type %s", sourcetype)).WithMetadata(map[string]interface{}{"err": err}).WithSeverity(events.Error).Build()
_ = provider.PersistEvent(event)
go h.config.EventBroadcaster.Publish(userUUID, event)
h.log.Error(ErrRequestBody(err))
http.Error(rw, ErrRequestBody(err).Error(), http.StatusBadRequest)
}

token, _ := r.Context().Value(models.TokenCtxKey).(string)

mesheryPattern := &models.MesheryPattern{} // pattern to be saved in the database
if parsedBody.PatternData == nil {
err := ErrRequestBody(fmt.Errorf("pattern_data cannot be empty, provide a valid pattern id for conversion"))

event := eventBuilder.WithDescription(fmt.Sprintf("Unable to convert design of type %s", sourcetype)).WithMetadata(map[string]interface{}{"err": err}).WithSeverity(events.Error).Build()
_ = provider.PersistEvent(event)
go h.config.EventBroadcaster.Publish(userUUID, event)
// Verifies and converts a pattern to design format if required.
// A pattern is required to be converted to design format iff,
// 1. pattern_file attribute is empty, and
// 2. The "type" (sourcetype/original content) is not Design. [is one of compose/helmchart/manifests]

h.log.Error(err)
http.Error(rw, err.Error(), http.StatusBadRequest)
}

mesheryPattern.Name = parsedBody.PatternData.Name
mesheryPattern.ID = parsedBody.PatternData.ID
func (h *Handler) VerifyAndConvertToDesign(
ctx context.Context,
mesheryPattern *models.MesheryPattern,
provider models.Provider,
) error {

eventBuilder.ActedUpon(*mesheryPattern.ID)
if mesheryPattern.Type.Valid && mesheryPattern.Type.String != string(models.Design) && mesheryPattern.PatternFile == "" {
token, _ := ctx.Value(models.TokenCtxKey).(string)

if parsedBody.PatternData.Location == nil {
parsedBody.PatternData.Location = map[string]interface{}{
"host": "",
"path": "",
"type": "local",
"branch": "",
sourceContent, err := provider.GetDesignSourceContent(token, mesheryPattern.ID.String())
if err != nil {
return err
}
}

sourceContent, err := provider.GetDesignSourceContent(r, mesheryPattern.ID.String())
if err != nil {
event := eventBuilder.WithDescription(fmt.Sprintf("Unable to convert design of type %s", sourcetype)).WithMetadata(map[string]interface{}{"err": err}).WithSeverity(events.Error).Build()
_ = provider.PersistEvent(event)
go h.config.EventBroadcaster.Publish(userUUID, event)
h.log.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}

mesheryPattern.SourceContent = sourceContent
mesheryPattern.SourceContent = sourceContent
sourcetype := mesheryPattern.Type.String

if sourcetype == string(models.DockerCompose) || sourcetype == string(models.K8sManifest) {
var k8sres string
if sourcetype == string(models.DockerCompose) {
k8sres, err = kompose.Convert(sourceContent) // convert the docker compose file into kubernetes manifest
if err != nil {
err = ErrConvertingDockerComposeToDesign(err)
event := eventBuilder.WithDescription(fmt.Sprintf("Unable to convert design of type %s", sourcetype)).WithMetadata(map[string]interface{}{"err": err}).WithSeverity(events.Error).Build()
_ = provider.PersistEvent(event)
go h.config.EventBroadcaster.Publish(userUUID, event)
if sourcetype == string(models.DockerCompose) || sourcetype == string(models.K8sManifest) {
var k8sres string
if sourcetype == string(models.DockerCompose) {
k8sres, err = kompose.Convert(sourceContent) // convert the docker compose file into kubernetes manifest
if err != nil {
err = ErrConvertingDockerComposeToDesign(err)
return err
}

h.log.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
} else if sourcetype == string(models.K8sManifest) {
k8sres = string(sourceContent)
}
mesheryPattern.Type = sql.NullString{
String: string(models.DockerCompose),
Valid: true,
pattern, err := pCore.NewPatternFileFromK8sManifest(k8sres, false, h.registryManager)
if err != nil {
err = ErrConvertingK8sManifestToDesign(err)
return err
}
} else if sourcetype == string(models.K8sManifest) {
k8sres = string(sourceContent)
mesheryPattern.Type = sql.NullString{
String: string(models.K8sManifest),
Valid: true,
response, err := yaml.Marshal(pattern)
if err != nil {
err = ErrMarshallingDesignIntoYAML(err)
return err
}
mesheryPattern.PatternFile = string(response)
}
pattern, err := pCore.NewPatternFileFromK8sManifest(k8sres, false, h.registryManager)
if err != nil {
err = ErrConvertingK8sManifestToDesign(err)
event := eventBuilder.WithDescription(fmt.Sprintf("Unable to convert design of type %s", sourcetype)).WithMetadata(map[string]interface{}{"err": err}).WithSeverity(events.Error).Build()
_ = provider.PersistEvent(event)
go h.config.EventBroadcaster.Publish(userUUID, event)

h.log.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
response, err := yaml.Marshal(pattern)
resp, err := provider.SaveMesheryPattern(token, mesheryPattern)
if err != nil {
err = ErrMarshallingDesignIntoYAML(err)
event := eventBuilder.WithDescription(fmt.Sprintf("Unable to convert design of type %s", sourcetype)).WithMetadata(map[string]interface{}{"err": err}).WithSeverity(events.Error).Build()
_ = provider.PersistEvent(event)
go h.config.EventBroadcaster.Publish(userUUID, event)

h.log.Error((err))
http.Error(rw, err.Error(), http.StatusInternalServerError)

return
obj := "save"
saveErr := ErrApplicationFailure(err, obj)
return saveErr
}
mesheryPattern.PatternFile = string(response)
}

resp, err := provider.SaveMesheryPattern(token, mesheryPattern)
if err != nil {
obj := "save"
saveErr := ErrApplicationFailure(err, obj)
h.log.Error(saveErr)
http.Error(rw, saveErr.Error(), http.StatusInternalServerError)
return
}

contentMesheryPatternSlice := make([]models.MesheryPattern, 0)
contentMesheryPatternSlice := make([]models.MesheryPattern, 0)

if err := json.Unmarshal(resp, &contentMesheryPatternSlice); err != nil {
http.Error(rw, ErrDecodePattern(err).Error(), http.StatusInternalServerError)
return
}

if len(contentMesheryPatternSlice) > 0 {
event := eventBuilder.WithDescription(fmt.Sprintf("Converted %s \"%s\" to Design format", sourcetype, contentMesheryPatternSlice[0].Name)).WithSeverity(events.Success).Build()
_ = provider.PersistEvent(event)
go h.config.EventBroadcaster.Publish(userUUID, event)
if err := json.Unmarshal(resp, &contentMesheryPatternSlice); err != nil {
return models.ErrUnmarshal(err, "pattern")
}
}

rw.Header().Set("Content-Type", "application/json")
fmt.Fprint(rw, string(resp))
return nil
}

func unCompressOCIArtifactIntoDesign(artifact []byte) (*models.MesheryPattern, error) {
Expand Down Expand Up @@ -1213,6 +1136,16 @@ func (h *Handler) DownloadMesheryPatternHandler(
return
}

err = h.VerifyAndConvertToDesign(r.Context(), pattern, provider)
if err != nil {
event := events.NewEvent().ActedUpon(*pattern.ID).FromSystem(*h.SystemID).FromUser(userID).WithCategory("pattern").WithAction("convert").WithDescription(fmt.Sprintf("The \"%s\" is not in the design format, failed to convert and persist the original source content from \"%s\" to design file format", pattern.Name, pattern.Type.String)).WithMetadata(map[string]interface{}{"error": err}).Build()
_ = provider.PersistEvent(event)
go h.config.EventBroadcaster.Publish(userID, event)
h.log.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}

if ociFormat {
tmpDir, err := oci.CreateTempOCIContentDir()
if err != nil {
Expand Down Expand Up @@ -1620,10 +1553,11 @@ func (h *Handler) GetMesheryPatternHandler(
rw http.ResponseWriter,
r *http.Request,
_ *models.Preference,
_ *models.User,
user *models.User,
provider models.Provider,
) {
patternID := mux.Vars(r)["id"]
userID := uuid.FromStringOrNil(user.ID)

resp, err := provider.GetMesheryPattern(r, patternID, r.URL.Query().Get("metrics"))
if err != nil {
Expand All @@ -1632,6 +1566,23 @@ func (h *Handler) GetMesheryPatternHandler(
return
}

pattern := &models.MesheryPattern{}
err = json.Unmarshal(resp, &pattern)
if err != nil {
h.log.Error(ErrGetPattern(err))
http.Error(rw, ErrGetPattern(err).Error(), http.StatusInternalServerError)
return
}
err = h.VerifyAndConvertToDesign(r.Context(), pattern, provider)
if err != nil {
event := events.NewEvent().ActedUpon(*pattern.ID).FromSystem(*h.SystemID).FromUser(userID).WithCategory("pattern").WithAction("convert").WithDescription(fmt.Sprintf("The \"%s\" is not in the design format, failed to convert and persist the original source content from \"%s\" to design file format", pattern.Name, pattern.Type.String)).WithMetadata(map[string]interface{}{"error": err}).Build()
_ = provider.PersistEvent(event)
go h.config.EventBroadcaster.Publish(userID, event)
h.log.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}

rw.Header().Set("Content-Type", "application/json")
fmt.Fprint(rw, string(resp))
}
Expand Down Expand Up @@ -1993,7 +1944,9 @@ func (h *Handler) GetMesheryPatternSourceHandler(
provider models.Provider,
) {
designID := mux.Vars(r)["id"]
resp, err := provider.GetDesignSourceContent(r, designID)
token, _ := r.Context().Value(models.TokenCtxKey).(string)

resp, err := provider.GetDesignSourceContent(token, designID)
if err != nil {
h.log.Error(ErrGetPattern(err))
http.Error(rw, ErrGetPattern(err).Error(), http.StatusNotFound)
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions server/meshmodel/consul/1.4.3/v1.0.0/model.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"id": "00000000-0000-0000-0000-000000000000",
"schemaVersion": "core.meshery.io/v1beta1",
"version": "v1.0.0",
"name": "consul",
"displayName": "Consul",
"description": "",
"status": "ignored",
"hostID": "00000000-0000-0000-0000-000000000000",
"registrant": {
"hostname": "artifacthub"
},
"category": {
"name": "Cloud Native Network",
"metadata": null
},
"subCategory": "Service Mesh",
"metadata": {
"capabilities": "",
"defaultData": "",
"isAnnotation": false,
"primaryColor": "#D62783",
"secondaryColor": "#ed74b4",
"shape": "circle",
"shapePolygonPoints": "",
"styleOverrides": "",
"styles": "",
"svgColor": "\u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\u003e\u003cg clip-path=\"url(#a)\" fill=\"#e03875\"\u003e\u003cpath fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9.626 12.114c-1.17 0-2.082-.94-2.082-2.147 0-1.208.911-2.148 2.082-2.148 1.17 0 2.081.94 2.081 2.148 0 1.208-.91 2.147-2.081 2.147Zm4.064-1.14a.994.994 0 0 1-.975-1.007c0-.537.423-1.007.976-1.007.52 0 .975.436.975 1.007a.994.994 0 0 1-.975 1.006Zm3.513.939a.947.947 0 0 1-1.17.704c-.521-.134-.814-.67-.684-1.208a.948.948 0 0 1 1.17-.705.985.985 0 0 1 .716 1.141s0 .034-.033.067h.001Zm-.684-2.551c-.52.134-1.04-.202-1.17-.739a1.002 1.002 0 0 1 .715-1.208c.52-.134 1.04.202 1.17.739a.843.843 0 0 1 0 .402.91.91 0 0 1-.715.806Zm3.446 2.416c-.098.536-.585.906-1.105.805a.984.984 0 0 1-.78-1.14c.097-.538.584-.907 1.105-.806.488.1.846.57.813 1.073-.032.034-.032.034-.032.068Zm-.779-2.482c-.52.1-1.008-.269-1.105-.806a.984.984 0 0 1 .78-1.14c.52-.101 1.008.268 1.106.805 0 .1.033.168 0 .268a.951.951 0 0 1-.78.873Zm-.682 5.94a.941.941 0 0 1-1.301.368 1.005 1.005 0 0 1-.358-1.342.941.941 0 0 1 1.3-.37c.326.202.521.571.489.94a1.934 1.934 0 0 1-.13.403Zm-.358-9.128a.941.941 0 0 1-1.3-.37 1.005 1.005 0 0 1 .357-1.342.941.941 0 0 1 1.3.37c.098.2.13.369.13.57a1.007 1.007 0 0 1-.487.772\"/\u003e\u003cpath d=\"M9.658 20c-2.601 0-5.008-1.04-6.861-2.92A10.35 10.35 0 0 1 0 10c0-2.685 1.008-5.168 2.83-7.081C4.65 1.04 7.09 0 9.657 0a9.4 9.4 0 0 1 5.886 2.047l-1.203 1.61c-1.364-1.071-2.99-1.642-4.68-1.642-2.049 0-4 .839-5.463 2.349C2.733 5.873 1.952 7.853 1.952 10a8.08 8.08 0 0 0 2.277 5.638 7.5 7.5 0 0 0 5.463 2.315 7.4 7.4 0 0 0 4.683-1.644l1.17 1.61C13.855 19.261 11.805 20 9.66 20Z\"/\u003e\u003c/g\u003e\u003cdefs\u003e\u003cclipPath id=\"a\"\u003e\u003cpath fill=\"#fff\" d=\"M0 0h20v20H0z\"/\u003e\u003c/clipPath\u003e\u003c/defs\u003e\u003c/svg\u003e",
"svgComplete": "",
"svgWhite": "\u003csvg width=\"32\" height=\"32\" viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\u003e\u003cg clip-path=\"url(#a)\" fill=\"#fff\"\u003e\u003cpath fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M15.401 19.383c-1.873 0-3.33-1.503-3.33-3.436s1.457-3.436 3.33-3.436 3.33 1.503 3.33 3.436-1.457 3.436-3.33 3.436Zm6.504-1.825c-.832 0-1.56-.698-1.56-1.611 0-.86.675-1.61 1.56-1.61.832 0 1.561.697 1.561 1.61 0 .913-.728 1.61-1.56 1.61Zm5.62 1.503a1.516 1.516 0 0 1-1.874 1.126c-.832-.214-1.3-1.073-1.092-1.932a1.516 1.516 0 0 1 1.873-1.128c.779.214 1.3.966 1.144 1.825 0 0 0 .054-.052.107v.002Zm-1.095-4.082c-.833.214-1.665-.323-1.873-1.182a1.604 1.604 0 0 1 1.145-1.933c.832-.215 1.664.323 1.872 1.182.052.215.052.43 0 .644-.052.59-.468 1.128-1.144 1.289Zm5.515 3.865c-.157.859-.938 1.45-1.77 1.289a1.574 1.574 0 0 1-1.248-1.826c.155-.858.936-1.45 1.768-1.288.781.16 1.354.912 1.302 1.718-.052.053-.052.053-.052.107Zm-1.247-3.971c-.832.161-1.613-.43-1.769-1.288a1.575 1.575 0 0 1 1.25-1.826c.832-.161 1.612.43 1.768 1.289 0 .16.052.268 0 .429-.052.698-.572 1.289-1.249 1.396Zm-1.092 9.503c-.416.752-1.353 1.02-2.082.59-.728-.429-.988-1.395-.572-2.147.416-.752 1.353-1.02 2.081-.59.52.322.833.912.78 1.503-.051.215-.103.43-.207.644Zm-.573-14.603c-.728.43-1.665.16-2.08-.591-.417-.752-.157-1.718.572-2.148.728-.43 1.664-.16 2.08.591.157.322.209.59.209.913-.052.483-.312.966-.78 1.235\"/\u003e\u003cpath d=\"M15.453 32c-4.162 0-8.013-1.664-10.978-4.67A16.56 16.56 0 0 1 0 16c0-4.296 1.613-8.27 4.527-11.33C7.44 1.664 11.343 0 15.453 0c3.434 0 6.712 1.127 9.418 3.276l-1.924 2.576c-2.186-1.717-4.788-2.63-7.493-2.63-3.278 0-6.4 1.343-8.741 3.759-2.34 2.416-3.59 5.584-3.59 9.02 0 3.382 1.301 6.604 3.643 9.02 2.341 2.415 5.411 3.704 8.741 3.704 2.758 0 5.36-.913 7.492-2.63l1.874 2.576c-2.706 2.147-5.984 3.328-9.418 3.328l-.002.001Z\"/\u003e\u003c/g\u003e\u003cdefs\u003e\u003cclipPath id=\"a\"\u003e\u003cpath fill=\"#fff\" d=\"M0 0h32v32H0z\"/\u003e\u003c/clipPath\u003e\u003c/defs\u003e\u003c/svg\u003e"
},
"model": {
"version": "1.4.3"
},
"components": null,
"relationships": null
}

0 comments on commit 8a87a61

Please sign in to comment.