Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/Dockerfile.local
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ RUN apt-get update && apt-get install -y \
libssh2-1 \
libssl3 \
ca-certificates \
# gitextractor shells out to the git CLI unless
# USE_GO_GIT_IN_GIT_EXTRACTOR is set, so the binary must be present or every
# clone fails with "git: executable file not found in $PATH".
# debian:bookworm-slim does not ship it; the official image inherits it from
# its python base.
git \
&& rm -rf /var/lib/apt/lists/*

# Copy libgit2
Expand Down
85 changes: 85 additions & 0 deletions backend/plugins/kiro/api/blueprint_v200.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 api

import (
"github.com/apache/incubator-devlake/core/errors"
coreModels "github.com/apache/incubator-devlake/core/models"
"github.com/apache/incubator-devlake/core/plugin"
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/helpers/srvhelper"
"github.com/apache/incubator-devlake/plugins/kiro/models"
"github.com/apache/incubator-devlake/plugins/kiro/tasks"
)

func MakeDataSourcePipelinePlanV200(
subtaskMetas []plugin.SubTaskMeta,
connectionId uint64,
bpScopes []*coreModels.BlueprintScope,
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
if err != nil {
return nil, nil, err
}
scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes)
if err != nil {
return nil, nil, err
}

plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, scopeDetails, connection)
if err != nil {
return nil, nil, err
}

// No domain layer scopes: this plugin writes only to _tool_kiro_* tables.
// Cross-tool AI modelling in the domain layer is separate work.
return plan, []plugin.Scope{}, nil
}

func makeDataSourcePipelinePlanV200(
subtaskMetas []plugin.SubTaskMeta,
scopeDetails []*srvhelper.ScopeDetail[models.KiroS3Slice, srvhelper.NoScopeConfig],
connection *models.KiroConnection,
) (coreModels.PipelinePlan, errors.Error) {
plan := make(coreModels.PipelinePlan, len(scopeDetails))
for i, scopeDetail := range scopeDetails {
slice := scopeDetail.Scope

op := &tasks.KiroOptions{
ConnectionId: slice.ConnectionId,
ScopeId: slice.Id,
AccountId: slice.AccountId,
Year: slice.Year,
Month: slice.Month,
}

// An empty entity list enables every subtask; the three streams are
// always collected together because they describe the same activity.
task, err := helper.MakePipelinePlanTask("kiro", subtaskMetas, []string{}, op)
if err != nil {
return nil, err
}

stage := plan[i]
if stage == nil {
stage = coreModels.PipelineStage{}
}
plan[i] = append(stage, task)
}
return plan, nil
}
163 changes: 163 additions & 0 deletions backend/plugins/kiro/api/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 api

import (
"net/http"

"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/plugins/kiro/models"
)

// PostConnections creates a new connection.
// @Summary create kiro connection
// @Description Create kiro connection
// @Tags plugins/kiro
// @Param body body models.KiroConnection true "json body"
// @Success 200 {object} models.KiroConnection
// @Failure 400 {object} shared.ApiBody "Bad Request"
// @Failure 500 {object} shared.ApiBody "Internal Error"
// @Router /plugins/kiro/connections [POST]
func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
connection := &models.KiroConnection{}
// Wrapped as BadInput so a struct-tag validation failure reports 400 rather
// than 500 - the difference between "fix your input" and "the server broke".
if err := api.Decode(input.Body, connection, vld); err != nil {
return nil, errors.BadInput.Wrap(err, "invalid connection payload")
}
if err := validateConnection(&connection.KiroConn); err != nil {
return nil, errors.BadInput.Wrap(err, "connection validation failed")
}
if err := connectionHelper.Create(connection, input); err != nil {
return nil, err
}
return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: http.StatusOK}, nil
}

// PatchConnection updates an existing connection.
// @Summary patch kiro connection
// @Description Patch kiro connection
// @Tags plugins/kiro
// @Param id path int true "connection ID"
// @Param body body models.KiroConnection true "json body"
// @Success 200 {object} models.KiroConnection
// @Failure 400 {object} shared.ApiBody "Bad Request"
// @Failure 500 {object} shared.ApiBody "Internal Error"
// @Router /plugins/kiro/connections/{id} [PATCH]
func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
connection := &models.KiroConnection{}
if err := connectionHelper.First(connection, input.Params); err != nil {
return nil, err
}
if err := (&models.KiroConnection{}).MergeFromRequest(connection, input.Body); err != nil {
return nil, errors.Convert(err)
}
if err := validateConnection(&connection.KiroConn); err != nil {
return nil, errors.BadInput.Wrap(err, "connection validation failed")
}
if err := connectionHelper.SaveWithCreateOrUpdate(connection); err != nil {
return nil, err
}
return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: http.StatusOK}, nil
}

// DeleteConnection removes a connection.
// @Summary delete a kiro connection
// @Description Delete a kiro connection
// @Tags plugins/kiro
// @Param id path int true "connection ID"
// @Success 200 {object} models.KiroConnection
// @Failure 400 {object} shared.ApiBody "Bad Request"
// @Failure 409 {object} srvhelper.DsRefs "References exist to this connection"
// @Failure 500 {object} shared.ApiBody "Internal Error"
// @Router /plugins/kiro/connections/{id} [DELETE]
func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
conn := &models.KiroConnection{}
output, err := connectionHelper.Delete(conn, input)
if err != nil {
return output, err
}
output.Body = conn.Sanitize()
return output, nil
}

// ListConnections lists all connections.
// @Summary get all kiro connections
// @Description Get all kiro connections
// @Tags plugins/kiro
// @Success 200 {object} []models.KiroConnection
// @Failure 400 {object} shared.ApiBody "Bad Request"
// @Failure 500 {object} shared.ApiBody "Internal Error"
// @Router /plugins/kiro/connections [GET]
func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
var connections []models.KiroConnection
if err := connectionHelper.List(&connections); err != nil {
return nil, err
}
for i := range connections {
connections[i] = connections[i].Sanitize()
}
return &plugin.ApiResourceOutput{Body: connections}, nil
}

// GetConnection returns one connection.
// @Summary get kiro connection detail
// @Description Get kiro connection detail
// @Tags plugins/kiro
// @Param id path int true "connection ID"
// @Success 200 {object} models.KiroConnection
// @Failure 400 {object} shared.ApiBody "Bad Request"
// @Failure 500 {object} shared.ApiBody "Internal Error"
// @Router /plugins/kiro/connections/{id} [GET]
func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
connection := &models.KiroConnection{}
err := connectionHelper.First(connection, input.Params)
if err != nil {
return nil, err
}
return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
}

// validateConnection checks the fields collection cannot proceed without.
//
// Identity Store fields are deliberately not required: they only resolve display
// names, and identity for joining to git history comes from the report's
// User_Email column. Requiring them would block a working setup.
func validateConnection(conn *models.KiroConn) error {
if conn.AccessKeyId == "" {
return errors.BadInput.New("AccessKeyId is required")
}
if conn.SecretAccessKey == "" {
return errors.BadInput.New("SecretAccessKey is required")
}
if conn.Region == "" {
return errors.BadInput.New("Region is required")
}
if conn.Bucket == "" {
return errors.BadInput.New("Bucket is required")
}
// A partial Identity Store configuration is a mistake worth reporting: it
// silently yields no display names, which looks like a data problem rather
// than a configuration one.
if (conn.IdentityStoreId == "") != (conn.IdentityStoreRegion == "") {
return errors.BadInput.New("IdentityStoreId and IdentityStoreRegion must be set together")
}
return nil
}
66 changes: 66 additions & 0 deletions backend/plugins/kiro/api/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 api

import (
"github.com/go-playground/validator/v10"

"github.com/apache/incubator-devlake/core/context"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/helpers/srvhelper"
"github.com/apache/incubator-devlake/plugins/kiro/models"
)

var (
vld *validator.Validate
connectionHelper *api.ConnectionApiHelper
basicRes context.BasicRes
// Scope config is NoScopeConfig: the report CSV's meaning is fixed by AWS
// and uniform across an organization, so there is nothing per-scope to
// configure.
dsHelper *api.DsHelper[models.KiroConnection, models.KiroS3Slice, srvhelper.NoScopeConfig]
)

func Init(br context.BasicRes, p plugin.PluginMeta) {
basicRes = br
vld = validator.New()
connectionHelper = api.NewConnectionHelper(
basicRes,
vld,
p.Name(),
)

dsHelper = api.NewDataSourceHelper[
models.KiroConnection, models.KiroS3Slice, srvhelper.NoScopeConfig,
](
basicRes,
p.Name(),
// Searchable scope fields.
[]string{"accountId", "name"},
func(c models.KiroConnection) models.KiroConnection { return c.Sanitize() },
func(s models.KiroS3Slice) models.KiroS3Slice { return s.Sanitize() },
nil,
)

// Scope browsing and search are implemented directly in remote_api.go rather
// than through the shared DsRemoteApiScopeList/Search helpers. Those build an
// HTTP client from the connection first, and that constructor runs a DNS
// check on the endpoint - a bucket name is not a hostname, so it fails. The
// helpers assume an HTTP data source; this one is S3.
}
Loading
Loading